blob: 1f6460d3fb9876cbea4e9589ed95138e961031f8 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jena.sparql.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.atlas.lib.tuple.Tuple;
import org.apache.jena.graph.Node;
import org.apache.jena.sparql.util.EqualityTest;
/**
* Simple isomorphism testing for collections of tuples of nodes. This can be used
* for graphs, datasets and results sets The Graph isomorphism code in Jena is much
* better (better tested, better performance) for graph isomorphism. This code is
* simple and slow but covers more cases.
*/
public class IsoAlg {
// Possible speed ups
// A/ Preprocess to look for constants and use a subset of tuples first.
/** Record the mapping of a mapping of a node. */
static class Mapping {
final Node node1;
final Node node2;
final Mapping parent;
static Mapping rootMapping = new Mapping(null, null, null);
public Mapping(Mapping parent, Node node1, Node node2) {
super();
this.parent = parent;
this.node1 = node1;
this.node2 = node2;
}
public boolean mapped(Node node) {
return map(node) != null;
}
public Node map(Node node) {
Mapping mapping = this;
while (mapping != rootMapping) {
if ( mapping.node1.equals(node) )
return mapping.node2;
mapping = mapping.parent;
}
return null;
}
public boolean reverseMapped(Node node) {
return reverseMap(node) != null;
}
public Node reverseMap(Node node) {
Mapping mapping = this;
while (mapping != rootMapping) {
if ( mapping.node2.equals(node) )
return mapping.node1;
mapping = mapping.parent;
}
return null;
}
@Override
public String toString() {
StringBuilder sbuff = new StringBuilder();
Mapping mapping = this;
while (mapping != rootMapping) {
sbuff.append("{" + mapping.node1 + " => " + mapping.node2 + "}");
mapping = mapping.parent;
}
sbuff.append("{}");
return sbuff.toString();
}
}
static class Possibility {
final Tuple<Node> tuple;
final Mapping mapping;
public Possibility(Tuple<Node> tuple, Mapping mapping) {
super();
this.tuple = tuple;
this.mapping = mapping;
}
@Override
public String toString() {
return String.format("Poss|%s %s|", tuple, mapping);
}
}
/**
* Blank node isomorphism test.
* Are the two collections of tuples of nodes isomorphic?
* In addition, when as two nodes considered "equal" (whether by
* {@link NodeUtils#sameValue} (SPARQL valuet testing), {@link NodeUtils#sameNode} (Node.equals),
* {@link NodeUtils#sameRdfTerm} (Node.equals, with lang tag insensitive testing).
*/
public static boolean isIsomorphic(Collection<Tuple<Node>> x1, Collection<Tuple<Node>> x2, EqualityTest nodeTest) {
return isIsomorphic(x1, x2, Iso.mappableBlankNodes, nodeTest);
}
/**
* Isomorphism test based on a class of mappable elements (e.g. blank nodes {@linkplain Iso#mappableBlankNodes},
* or blank nodes and variables {@linkplain Iso#mappableBlankNodesVariables}).
* In addition, when are two nodes considered "equal" (whether by
* {@link NodeUtils#sameValue} (SPARQL value testing), {@link NodeUtils#sameNode} (Node.equals),
* {@link NodeUtils#sameRdfTerm} (Node.equals, with lang tag insensitive testing).
*/
public static boolean isIsomorphic(Collection<Tuple<Node>> x1, Collection<Tuple<Node>> x2, Iso.Mappable mappable, EqualityTest nodeTest) {
return matcher(x1, x2, Mapping.rootMapping, mappable, nodeTest);
}
// Debug.
// XXX Restore private static final
public static boolean DEBUG = false;
private static final IndentedWriter out = new IndentedWriter(System.out);
static {
out.setFlushOnNewline(true);
}
private static boolean matcher(Collection<Tuple<Node>> tuples1, Collection<Tuple<Node>> tuples2, Mapping mapping, Iso.Mappable mappable,
EqualityTest nodeTest) {
if ( DEBUG ) {
out.println("match: ");
out.println(" 1: " + tuples1);
out.println(" 2: " + tuples2);
out.println(" M: " + mapping);
}
if ( tuples1.size() != tuples2.size() )
return false;
// List-copy, these will be mutated.
tuples2 = new ArrayList<>(tuples2);
tuples1 = new ArrayList<>(tuples1);
for ( Tuple<Node> t1 : tuples1 ) {
if ( DEBUG )
out.println(" Process t1 = " + t1);
tuples1.remove(t1);
List<Possibility> causes = matcher(t1, tuples2, mapping, mappable, nodeTest);
if ( DEBUG )
out.println(" Possibilities: Tuple" + t1 + " :: " + causes);
out.incIndent();
try {
// Try each possible tuple-tuple matching until one succeeds all the
// way.
for ( Possibility c : causes ) {
if ( DEBUG )
out.println(" Try: " + c);
// Try t1 -> t2
Tuple<Node> t2 = c.tuple;
tuples2.remove(t2);
// Try without t1 and t2, using the mapping of this cause.
if ( tuples1.isEmpty() && tuples2.isEmpty() ) // They are the
// same size.
return true;
// Recurse
if ( matcher(tuples1, tuples2, c.mapping, mappable, nodeTest) ) {
if ( DEBUG )
out.println("Yes");
return true;
}
if ( DEBUG )
out.println("No");
tuples2.add(t2);
}
return false;
}
finally {
out.decIndent();
}
}
// The empty-empty case
return true;
}
/** Return all possible tuple-tuple matches from tuple t1 to tuples in x2 */
private static List<Possibility> matcher(Tuple<Node> t1, Collection<Tuple<Node>> g2, Mapping mapping, Iso.Mappable mappable,
EqualityTest nodeTest) {
List<Possibility> matches = new ArrayList<>();
for ( Tuple<Node> t2 : g2 ) {
// No - multiple bNodes.
Mapping step = gen(t1, t2, mapping, mappable, nodeTest);
if ( step != null ) {
Possibility c = new Possibility(t2, step);
matches.add(c);
}
}
return matches;
}
/**
* Find a mapping between the tuples, given a start mapping.
* Return a mapping or null for "no match".
*/
private static Mapping gen(Tuple<Node> t1, Tuple<Node> t2, Mapping _mapping, Iso.Mappable mappable, EqualityTest nodeTest) {
if ( t1.len() != t2.len() )
return null;
Mapping mapping = _mapping;
for ( int i = 0 ; i < t1.len() ; i++ ) {
Node n1 = t1.get(i);
Node n2 = t2.get(i);
Node n1m = mapping.map(n1);
if ( n1m != null ) {
// Already mapped
if ( n1m.equals(n2) )
// Exact equals after mapping t1 slot.
continue;
// No match.
return null;
}
// Not supported: mappable literals (or mapping any term where "same" is
// not "Node.equals").
if ( mappable.mappable(n1, n2) ) {
if ( mapping.reverseMapped(n2) ) {
// Already a target.
// but not the same (else n1m != null)
return null;
}
// **** If n2 not already mapped.
mapping = new Mapping(mapping, n1, n2);
continue;
}
if ( !nodeTest.equal(n1, n2) )
// No isomorphism.
return null;
}
return mapping;
}
}