diff --git a/.gitignore b/.gitignore index b07be1b34a7..7868e0e0e01 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ hs_err_* # Fuseki file area run/ + +# Benchmark JSON results +jena-benchmarks/jena-benchmarks-jmh/*.json + diff --git a/jena-arq/pom.xml b/jena-arq/pom.xml index 2939e959abf..a1698678ce9 100644 --- a/jena-arq/pom.xml +++ b/jena-arq/pom.xml @@ -45,7 +45,7 @@ com.google.code.gson gson - + org.slf4j @@ -71,12 +71,12 @@ jakarta.json - + com.google.protobuf protobuf-java - + org.apache.thrift libthrift @@ -124,6 +124,11 @@ test + + org.apache.commons + commons-math4-legacy + test + @@ -139,7 +144,7 @@ org.apache.maven.plugins maven-resources-plugin - + org.apache.maven.plugins maven-surefire-plugin @@ -172,11 +177,11 @@ org.apache.maven.plugins maven-source-plugin - + - attach-sources-test + attach-sources-test - test-jar-no-fork + test-jar-no-fork diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java index 1ca880b9c88..765334920ba 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java @@ -18,134 +18,22 @@ package org.apache.jena.rdfs; -import static org.apache.jena.atlas.iterator.Iter.iter; - -import java.util.Iterator; - -import org.apache.jena.atlas.iterator.Iter; -import org.apache.jena.graph.Graph; -import org.apache.jena.graph.Node; +import org.apache.jena.rdfs.engine.DatasetGraphWithGraphTransform; import org.apache.jena.sparql.core.DatasetGraph; -import org.apache.jena.sparql.core.DatasetGraphWrapper; import org.apache.jena.sparql.core.DatasetGraphWrapperView; -import org.apache.jena.sparql.core.Quad; import org.apache.jena.sparql.util.Context; -public class DatasetGraphRDFS extends DatasetGraphWrapper implements DatasetGraphWrapperView { +public class DatasetGraphRDFS extends DatasetGraphWithGraphTransform implements DatasetGraphWrapperView { // Do not unwrap for query execution. - private final SetupRDFS setup; public DatasetGraphRDFS(DatasetGraph dsg, SetupRDFS setup) { - super(dsg); + super(dsg, g -> new GraphRDFS(g, setup)); this.setup = setup; } public DatasetGraphRDFS(DatasetGraph dsg, SetupRDFS setup, Context cxt) { - super(dsg, cxt); + super(dsg, cxt, g -> new GraphRDFS(g, setup)); this.setup = setup; } - - // Graph-centric access. - @Override - public Graph getDefaultGraph() { - Graph base = getG().getDefaultGraph(); - return new GraphRDFS(base, setup); - } - - @Override - public Graph getUnionGraph() { - Graph base = getG().getUnionGraph(); - return new GraphRDFS(base, setup); - } - - @Override - public Graph getGraph(Node graphNode) { - Graph base = getG().getGraph(graphNode); - if ( base == null ) - return null; - return new GraphRDFS(base, setup); - } - - @Override - public Iterator find() - { return find(Node.ANY, Node.ANY, Node.ANY, Node.ANY); } - - // Quad-centric access - @Override - public Iterator find(Quad quad) { - return find(quad.getGraph(), quad.getSubject(), quad.getPredicate(), quad.getObject()); - } - - @Override - public Iterator find(Node g, Node s, Node p, Node o) { - Iterator iter = findInf(g, s, p, o); - if ( iter == null ) - return Iter.nullIterator(); - return iter; - } - -// private Iterator findInf(Node g, Node s, Node p, Node o) { -// // Puts in the graph name for the quad base don g even if g is ANY or null. -// MatchRDFS infMatcher = new InfFindQuad(setup, g, getR()); -// Stream quads = infMatcher.match(s, p, o); -// Iterator iter = quads.iterator(); -// iter = Iter.onClose(iter, ()->quads.close()); -// return iter; -// } - - /** - * Find, graph by graph. - */ - private Iterator findInf(Node g, Node s, Node p, Node o) { - if ( g != null && g.isConcrete() ) { - // Includes the union graph case. - return findOneGraphInf(g, s, p, o); - } - // Wildcard. Do each graph in-term. - // This ensures the graph node of the quad corresponds to where the inference came from. - Iter iter1 = findOneGraphInf(Quad.defaultGraphIRI, s, p, o); - Iterator iter2 = findAllNamedGraphInf(s, p, o); - return iter1.append(iter2); - } - - // All named graphs, with inference. Quads refer to the name graph they were caused by. - private Iterator findAllNamedGraphInf(Node s, Node p, Node o) { - return Iter.flatMap(listGraphNodes(), gn -> findOneGraphInf(gn, s, p, o)); - } - - // Single graph (inc. union graph). Quads refer to the name graph they were caused by. - private Iter findOneGraphInf(Node g, Node s, Node p, Node o) { - if ( ! g.isConcrete() ) - throw new IllegalStateException(); - // f ( Quad.isUnionGraph(g) ) {} - // Specific named graph. - return iter(getGraph(g).find(s,p,o)).map(t->Quad.create(g, t)); - } - - @Override - public Iterator findNG(Node g, Node s, Node p, Node o) { - if ( Quad.isDefaultGraph(g) ) - throw new IllegalArgumentException("Default graph in findNG call"); - if ( g == null ) - g = Node.ANY; - if ( g == Node.ANY ) - return findAllNamedGraphInf(s, p, o); - // Same as specific named graph - we return quads in the union graph. -// if ( Quad.isUnionGraph(g) ) {} - return findOneGraphInf(g, s, p, o); - } - - @Override - public boolean contains(Quad quad) - { return contains(quad.getGraph(), quad.getSubject(), quad.getPredicate(), quad.getObject()); } - - @Override - public boolean contains(Node g, Node s, Node p, Node o) { - // Go through the inference machinery. - Iterator iter = find(g, s, p, o); - try { - return iter.hasNext(); - } finally { Iter.close(iter); } - } } diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java index 10f736fbd58..c2089c1edf5 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java @@ -18,73 +18,20 @@ package org.apache.jena.rdfs; -import java.util.stream.Stream; - import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; -import org.apache.jena.graph.Triple; +import org.apache.jena.rdfs.engine.GraphMatch; import org.apache.jena.rdfs.engine.InfFindTriple; -import org.apache.jena.rdfs.engine.MatchRDFS; import org.apache.jena.rdfs.setup.ConfigRDFS; -import org.apache.jena.sparql.graph.GraphWrapper; -import org.apache.jena.util.iterator.ExtendedIterator; -import org.apache.jena.util.iterator.WrappedIterator; /** * RDFS graph over a base graph. */ -public class GraphRDFS extends GraphWrapper { - private final MatchRDFS source; +public class GraphRDFS extends GraphMatch { private final ConfigRDFS setup; public GraphRDFS(Graph graph, ConfigRDFS setup) { - super(graph); + super(graph, new InfFindTriple(setup, graph)); this.setup = setup; - this.source = new InfFindTriple(setup, graph); - } - - @Override - public ExtendedIterator find(Triple m) { - return find(m.getSubject(), m.getPredicate(), m.getObject()); - } - - @Override - public ExtendedIterator find(Node s, Node p, Node o) { - Stream stream = source.match(s, p, o); - ExtendedIterator iter = WrappedIterator.ofStream(stream); - return iter; - } - - @Override - public Stream stream(Node s, Node p, Node o) { - return source.match(s, p, o); - } - - @Override - public boolean contains(Node s, Node p, Node o) { - // Must go via find()-like functionality. - ExtendedIterator iter = find(s, p, o); - try { - return iter.hasNext(); - } finally { iter.close(); } - } - - @Override - public boolean contains(Triple t) { - return contains(t.getSubject(), t.getPredicate(), t.getObject()); - } - - @Override - public int size() { - // Report the size of the underlying graph. - // Even better, don't ask. - return super.size(); - } - - @Override - public boolean dependsOn(Graph other) { - if ( other == super.get() ) - return true; - return super.dependsOn(other); } } diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java b/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java index 37e42fe723c..034d73eb8ae 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java @@ -18,8 +18,12 @@ package org.apache.jena.rdfs; import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; +import org.apache.jena.rdfs.engine.MapperX; +import org.apache.jena.rdfs.setup.BaseSetupRDFS; +import org.apache.jena.rdfs.setup.ConfigRDFS; import org.apache.jena.riot.system.StreamRDF; import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.sparql.util.NodeUtils; @@ -56,13 +60,13 @@ public static DatasetGraph datasetRDFS(DatasetGraph data, SetupRDFS setup) { /** Create an RDFS inference dataset. */ public static DatasetGraph datasetRDFS(DatasetGraph data, Graph vocab ) { SetupRDFS setup = setupRDFS(vocab); - return new DatasetGraphRDFS(data, setup); + return datasetRDFS(data, setup); } /** Create an RDFS inference dataset. */ public static Dataset datasetRDFS(Dataset data, Graph vocab ) { SetupRDFS setup = setupRDFS(vocab); - return DatasetFactory.wrap(new DatasetGraphRDFS(data.asDatasetGraph(), setup)); + return DatasetFactory.wrap(datasetRDFS(data.asDatasetGraph(), setup)); } /** Create an {@link SetupRDFS} */ @@ -70,6 +74,16 @@ public static SetupRDFS setupRDFS(Graph vocab) { return new SetupRDFS(vocab); } + /** Create a {@link ConfigRDFS} via a {@link MapperX}. */ + public static ConfigRDFS setupRDFS(Graph vocab, MapperX mapper) { + return new BaseSetupRDFS<>(vocab) { + @Override + protected X fromNode(Node node) { + return mapper.fromNode(node); + } + }; + } + /** Stream expand data based on a separate vocabulary */ public static StreamRDF streamRDFS(StreamRDF data, Graph vocab) { SetupRDFS setup = new SetupRDFS(vocab); diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java index f82ec6c80e5..7ac921a05d0 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java @@ -26,7 +26,6 @@ import org.apache.jena.assembler.exceptions.AssemblerException; import org.apache.jena.graph.Graph; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdfs.DatasetGraphRDFS; import org.apache.jena.rdfs.RDFSFactory; import org.apache.jena.rdfs.SetupRDFS; import org.apache.jena.riot.RDFDataMgr; @@ -50,7 +49,7 @@ public Map pool() { /** *
      * <#rdfsDS> rdf:type ja:DatasetRDFS ;
-     *      ja:rdfs "vocab.ttl";
+     *      ja:rdfsSchema "vocab.ttl";
      *      ja:dataset <#baseDS> ;
      *      .
      *
@@ -58,7 +57,6 @@ public Map pool() {
      *     ja:name "TIM database"  # optional: this is need if the base database is accessed directly.
      *     ja:data "data1.trig";
      *     ## ja:data "data2.trig";
-     *
      *     .
      * 
*/ @@ -76,7 +74,7 @@ public DatasetGraph createDataset(Assembler a, Resource root) { Graph schema = RDFDataMgr.loadGraph(schemaFile); SetupRDFS setup = RDFSFactory.setupRDFS(schema); - DatasetGraph dsg = new DatasetGraphRDFS(base, setup); + DatasetGraph dsg = RDFSFactory.datasetRDFS(base, setup); AssemblerUtils.mergeContext(root, dsg.getContext()); return dsg; } diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java index f0869d31905..8d1315bf330 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java @@ -48,7 +48,7 @@ public Object open(Assembler a, Resource root, Mode mode) { /** *
      * <#rdfsGraph> rdf:type ja:GraphRDFS ;
-     *      ja:rdfs "vocab.ttl";
+     *      ja:rdfsSchema "vocab.ttl";
      *      ja:graph <#baseGraph> ;
      *      .
      *
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java
index 9d69662f51e..69943e75153 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java
@@ -129,10 +129,12 @@ final private void range(X s, X p, X o, Output out) {
                 return;
         }
         Set x = setup.getRange(p);
-        x.forEach(c -> {
-            derive(o, rdfType, c, out);
-            subClass(o, rdfType, c, out);
-        });
+        if (!mapper.isLiteral(o)) {
+            x.forEach(c -> {
+                derive(o, rdfType, c, out);
+                subClass(o, rdfType, c, out);
+            });
+        }
     }
 }
 
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/DatasetGraphWithGraphTransform.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/DatasetGraphWithGraphTransform.java
new file mode 100644
index 00000000000..aacd9c5ac17
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/DatasetGraphWithGraphTransform.java
@@ -0,0 +1,157 @@
+/*
+ * 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.rdfs.engine;
+
+import static org.apache.jena.atlas.iterator.Iter.iter;
+
+import java.util.Iterator;
+import java.util.function.Function;
+
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphWrapper;
+import org.apache.jena.sparql.core.DatasetGraphWrapperView;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.util.Context;
+
+public class DatasetGraphWithGraphTransform extends DatasetGraphWrapper implements DatasetGraphWrapperView {
+    // Do not unwrap for query execution.
+
+    private Function graphTransform;
+
+    public DatasetGraphWithGraphTransform(DatasetGraph dsg, Function graphTransform) {
+        super(dsg);
+        this.graphTransform = graphTransform;
+    }
+
+    public DatasetGraphWithGraphTransform(DatasetGraph dsg, Context cxt, Function graphTransform) {
+        super(dsg, cxt);
+        this.graphTransform = graphTransform;
+    }
+
+    private Graph wrapGraph(Graph graph) {
+        Graph result = graphTransform.apply(graph);
+        return result;
+    }
+
+    // Graph-centric access.
+    @Override
+    public Graph getDefaultGraph() {
+        Graph base = getG().getDefaultGraph();
+        return wrapGraph(base);
+    }
+
+    @Override
+    public Graph getUnionGraph() {
+        Graph base = getG().getUnionGraph();
+        return wrapGraph(base);
+    }
+
+    @Override
+    public Graph getGraph(Node graphNode) {
+        Graph base = getG().getGraph(graphNode);
+        if ( base == null )
+            return null;
+        return wrapGraph(base);
+    }
+
+    @Override
+    public Iterator find()
+    { return find(Node.ANY, Node.ANY, Node.ANY, Node.ANY); }
+
+    // Quad-centric access
+    @Override
+    public Iterator find(Quad quad) {
+        return find(quad.getGraph(), quad.getSubject(), quad.getPredicate(), quad.getObject());
+    }
+
+    @Override
+    public Iterator find(Node g, Node s, Node p, Node o) {
+        Iterator iter = findInf(g, s, p, o);
+        if ( iter == null )
+            return Iter.nullIterator();
+        return iter;
+    }
+
+//    private Iterator findInf(Node g, Node s, Node p, Node o) {
+//        // Puts in the graph name for the quad base don g even if g is ANY or null.
+//        MatchRDFS infMatcher = new InfFindQuad(setup, g, getR());
+//        Stream quads = infMatcher.match(s, p, o);
+//        Iterator iter = quads.iterator();
+//        iter = Iter.onClose(iter, ()->quads.close());
+//        return iter;
+//    }
+
+    /**
+     * Find, graph by graph.
+     */
+    private Iterator findInf(Node g, Node s, Node p, Node o) {
+        if ( g != null && g.isConcrete() ) {
+            // Includes the union graph case.
+            return findOneGraphInf(g, s, p, o);
+        }
+        // Wildcard. Do each graph in-term.
+        // This ensures the graph node of the quad corresponds to where the inference came from.
+        Iter iter1 = findOneGraphInf(Quad.defaultGraphIRI, s, p, o);
+        Iterator iter2 = findAllNamedGraphInf(s, p, o);
+        return iter1.append(iter2);
+    }
+
+    // All named graphs, with inference. Quads refer to the name graph they were caused by.
+    private Iterator findAllNamedGraphInf(Node s, Node p, Node o) {
+        return Iter.flatMap(listGraphNodes(), gn -> findOneGraphInf(gn, s, p, o));
+    }
+
+    // Single graph (inc. union graph). Quads refer to the name graph they were caused by.
+    private Iter findOneGraphInf(Node g, Node s, Node p, Node o) {
+        if ( ! g.isConcrete()  )
+            throw new IllegalStateException();
+        // f ( Quad.isUnionGraph(g) ) {}
+        // Specific named graph.
+        return iter(getGraph(g).find(s,p,o)).map(t->Quad.create(g, t));
+    }
+
+    @Override
+    public Iterator findNG(Node g, Node s, Node p, Node o) {
+        if ( Quad.isDefaultGraph(g) )
+            throw new IllegalArgumentException("Default graph in findNG call");
+        if ( g == null )
+            g = Node.ANY;
+        if ( g == Node.ANY )
+            return findAllNamedGraphInf(s, p, o);
+        // Same as specific named graph - we return quads in the union graph.
+//        if ( Quad.isUnionGraph(g) ) {}
+        return findOneGraphInf(g, s, p, o);
+    }
+
+    @Override
+    public boolean contains(Quad quad)
+    { return contains(quad.getGraph(), quad.getSubject(), quad.getPredicate(), quad.getObject()); }
+
+    @Override
+    public boolean contains(Node g, Node s, Node p, Node o) {
+        // Go through the inference machinery.
+        Iterator iter = find(g, s, p, o);
+        try {
+            return iter.hasNext();
+        } finally { Iter.close(iter); }
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java
index 0fcfa02e2dd..2eb3a4ea7ab 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java
@@ -18,7 +18,8 @@
 
 package org.apache.jena.rdfs.engine;
 
-import static org.apache.jena.rdfs.engine.ConstRDFS.*;
+import static org.apache.jena.rdfs.engine.ConstRDFS.rdfType;
+import static org.apache.jena.rdfs.engine.ConstRDFS.rdfsSubClassOf;
 
 import java.util.Set;
 import java.util.stream.Stream;
@@ -53,7 +54,6 @@ public GraphIncRDFS(Graph graph, ConfigRDFS setup) {
                 .filter(type->!setup.getSubClassHierarchy().keySet().contains(type))
                 .map(type->Triple.create(type, rdfsSubClassOf, type))
                 );
-
     }
 
     @Override
@@ -73,6 +73,11 @@ public ExtendedIterator find(Node s, Node p, Node o) {
         return iter;
     }
 
+    @Override
+    public boolean contains(Node s, Node p, Node o) {
+        return vocab.contains(s, p, o) || super.contains(s, p, o);
+    }
+
     private Stream extras(Node s, Node p, Node o) {
         return extra.stream().filter(t->
             matchTerm(t.getSubject(),s) && matchTerm(t.getPredicate(),p) && matchTerm(t.getObject(),o) );
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphMatch.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphMatch.java
new file mode 100644
index 00000000000..8e9eee0d7fa
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphMatch.java
@@ -0,0 +1,95 @@
+/*
+ * 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.rdfs.engine;
+
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.sparql.graph.GraphWrapper;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.WrappedIterator;
+
+/**
+ * A Graph view over a {@link Match}. A graph can be specified as a delegate
+ * for all functionality that is not covered by the Match.
+ */
+public class GraphMatch extends GraphWrapper {
+    private final Match source;
+
+    public GraphMatch(Graph graph, Match match) {
+        super(graph);
+        this.source = match;
+    }
+
+    /**
+     * Wrap a base graph such that its find() and contains() methods
+     * are delegated to the match.
+     * Other methods, such as those for updates, go to the base graph.
+     */
+    public static  Graph adapt(Graph baseGraph, Match match) {
+        return new GraphMatch(baseGraph, new MatchAdapter<>(match, match.getMapper()));
+    }
+
+    public Match getMatch() {
+        return source;
+    }
+
+    @Override
+    public ExtendedIterator find(Triple m) {
+        return find(m.getSubject(), m.getPredicate(), m.getObject());
+    }
+
+    @Override
+    public ExtendedIterator find(Node s, Node p, Node o) {
+        Stream stream = source.match(s, p, o);
+        ExtendedIterator iter = WrappedIterator.ofStream(stream);
+        return iter;
+    }
+
+    @Override
+    public Stream stream(Node s, Node p, Node o) {
+        return source.match(s, p, o);
+    }
+
+    @Override
+    public boolean contains(Node s, Node p, Node o) {
+        return source.contains(s, p, o);
+    }
+
+    @Override
+    public boolean contains(Triple t) {
+        return contains(t.getSubject(), t.getPredicate(), t.getObject());
+    }
+
+    @Override
+    public int size() {
+        // Report the size of the underlying graph.
+        // Even better, don't ask.
+        return super.size();
+    }
+
+    @Override
+    public boolean dependsOn(Graph other) {
+        if ( other == super.get() )
+            return true;
+        return super.dependsOn(other);
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java
index c9a771bd399..97d86206e10 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java
@@ -59,10 +59,4 @@ public Stream sourceFind(Node s, Node p, Node o) {
     protected boolean sourceContains(Node s, Node p, Node o) {
         return dsg.contains(graph, s, p, o);
     }
-
-    @Override
-    protected Quad dstCreate(Node s, Node p, Node o) {
-        // Must be concrete for this quad creation.
-        return Quad.create(graph, s, p, o);
-    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
index 9511acd0e16..2f152f3c3c8 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
@@ -50,9 +50,4 @@ public Stream sourceFind(Node s, Node p, Node o) {
     protected boolean sourceContains(Node s, Node p, Node o) {
         return graph.contains(s, p, o);
     }
-
-    @Override
-    protected Triple dstCreate(Node s, Node p, Node o) {
-        return Triple.create(s, p, o);
-    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
index 29a3650e476..bf136cdbbbc 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
@@ -28,4 +28,10 @@ public interface MapperX {
     public abstract X subject(T tuple);
     public abstract X predicate(T tuple);
     public abstract X object(T tuple);
+
+    public abstract T tuple(X s, X p, X o);
+
+    public default boolean isLiteral(X x) {
+        return toNode(x).isLiteral();
+    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java
index 5c70427c1e1..2666cc0a7c6 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java
@@ -19,6 +19,8 @@
 package org.apache.jena.rdfs.engine;
 
 import org.apache.jena.atlas.lib.tuple.Tuple;
+import org.apache.jena.atlas.lib.tuple.Tuple3;
+import org.apache.jena.atlas.lib.tuple.TupleFactory;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.sparql.core.Quad;
@@ -42,6 +44,7 @@ private static class MapperTriple implements MapperX {
         @Override public Node subject(Triple triple)    { return triple.getSubject(); }
         @Override public Node predicate(Triple triple)  { return triple.getPredicate(); }
         @Override public Node object(Triple triple)     { return triple.getObject(); }
+        @Override public Triple tuple(Node s, Node p, Node o) { return Triple.create(s, p, o); }
     }
 
     private static class MapperQuad implements MapperX {
@@ -52,6 +55,7 @@ private static class MapperQuad implements MapperX {
         @Override public Node subject(Quad quad)    { return quad.getSubject(); }
         @Override public Node predicate(Quad quad)  { return quad.getPredicate(); }
         @Override public Node object(Quad quad)     { return quad.getObject(); }
+        @Override public Quad tuple(Node s, Node p, Node o) { return Quad.create(graph, s, p, o); }
     }
 
     private static class MapperTuple implements MapperX> {
@@ -60,6 +64,7 @@ private static class MapperTuple implements MapperX> {
         @Override public Node subject(Tuple tuple)    { return offset(tuple, 0); }
         @Override public Node predicate(Tuple tuple)  { return offset(tuple, 1); }
         @Override public Node object(Tuple tuple)     { return offset(tuple, 2); }
+        @Override public Tuple3 tuple(Node s, Node p, Node o) { return TupleFactory.create3(s, p, o); }
 
         private static Node offset(Tuple tuple, int i) {
             int idx = ( tuple.len() == 3 ) ? i : i+1;
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
index 2fa7cf4151f..32601b45c78 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
@@ -20,10 +20,25 @@
 
 import java.util.stream.Stream;
 
+import org.apache.jena.graph.Node;
+
 /**
  * Match by S/P/O where {@code X} is the RDF term representation (Node, NodeId) and
  * {@code T} is the tuple (triple, quad, tuple) representation.
  */
 public interface Match {
     public Stream match(X s, X p, X o);
+
+    public default boolean contains(X s, X p, X o) {
+        try (Stream stream = match(s, p, o)) {
+            return stream.findFirst().isPresent();
+        }
+    }
+
+    /**
+     * The mapper for reuse with wrappers.
+     * Note that this indirectly ties the {@link Match} interface to the {@link Node} realm:
+     * One can use the mapper to obtain X for e.g. RDF.Nodes.type.
+     */
+    MapperX getMapper();
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchAdapter.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchAdapter.java
new file mode 100644
index 00000000000..0d4aaf5e799
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchAdapter.java
@@ -0,0 +1,72 @@
+/*
+ * 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.rdfs.engine;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+/**
+ * This is the bridge between the Node/Triple level and some lower level such as one based on NodeIds.
+ */
+public final class MatchAdapter
+    implements Match
+{
+    private Match below;
+    private MapperX mapper;
+
+    public MatchAdapter(Match below, MapperX mapper) {
+        super();
+        this.mapper = Objects.requireNonNull(mapper);
+        this.below = Objects.requireNonNull(below);
+    }
+
+    @Override
+    public Stream match(Node s, Node p, Node o) {
+        X sd = down(s);
+        X pd = down(p);
+        X od = down(o);
+        return below.match(sd, pd, od).map(this::up);
+    }
+
+    private X down(Node node) {
+        return mapper.fromNode(node);
+    }
+
+    private Triple up(T tuple) {
+        X sd = mapper.subject(tuple);
+        X pd = mapper.predicate(tuple);
+        X od = mapper.object(tuple);
+        Node s = mapper.toNode(sd);
+        Node p = mapper.toNode(pd);
+        Node o = mapper.toNode(od);
+        return dstCreate(s, p, o);
+    }
+
+    private Triple dstCreate(Node s, Node p, Node o) {
+        return Triple.create(s, p, o);
+    }
+
+    @Override
+    public MapperX getMapper() {
+        return Mappers.mapperTriple();
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchGraph.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchGraph.java
new file mode 100644
index 00000000000..40d064eed59
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchGraph.java
@@ -0,0 +1,55 @@
+/*
+ * 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.rdfs.engine;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+/**
+ * A {@link Match} view over a {@link Graph}.
+ * This class is final. Use {@link MatchWrapper} to modify match behavior.
+ */
+public final class MatchGraph
+    implements Match
+{
+    private Graph base;
+
+    public MatchGraph(Graph base) {
+        super();
+        this.base = Objects.requireNonNull(base);
+    }
+
+    public Graph getGraph() {
+        return base;
+    }
+
+    @Override
+    public Stream match(Node s, Node p, Node o) {
+        return base.stream(s, p, o);
+    }
+
+    @Override
+    public MapperX getMapper() {
+        return Mappers.mapperTriple();
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java
index cef8c1af9f8..9ab430f61df 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java
@@ -73,6 +73,11 @@ public MatchRDFS(ConfigRDFS setup, MapperX mapper) {
         };
     }
 
+    @Override
+    public MapperX getMapper() {
+        return mapper;
+    }
+
     @Override
     public final Stream match(X s, X p, X o) { return matchWithInf(s, p ,o); }
 
@@ -84,7 +89,6 @@ public MatchRDFS(ConfigRDFS setup, MapperX mapper) {
     // Access data.
     protected abstract boolean sourceContains(X s, X p, X o);
     protected abstract Stream sourceFind(X s, X p, X o);
-    protected abstract T dstCreate(X s, X p, X o);
 
     protected final X subject(T tuple)        { return mapper.subject(tuple); }
     protected final X predicate(T tuple)      { return mapper.predicate(tuple); }
@@ -460,6 +464,11 @@ private static  boolean isEmpty(Map map) {
         return map == null || map.isEmpty();
     }
 
+    /** Inherit tuple construction from the mapper. */
+    protected T dstCreate(X s, X p, X o) {
+        return getMapper().tuple(s, p, o);
+    }
+
 //  private void print(Map> map) {
 //      System.out.println("{");
 //      CollectionUtils.forEach(map, (k,v)->System.out.printf("  %-20s  %s\n", k, v));
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFSWrapper.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFSWrapper.java
new file mode 100644
index 00000000000..eee372f35e7
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFSWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.rdfs.engine;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.jena.rdfs.setup.ConfigRDFS;
+
+/** MatchRDFS implementation as a wrapper over another Match source. */
+public class MatchRDFSWrapper
+    extends MatchRDFS
+{
+    protected Match source;
+
+    public MatchRDFSWrapper(ConfigRDFS setup, Match source) {
+        super(setup, source.getMapper());
+        this.source = Objects.requireNonNull(source);
+    }
+
+    @Override
+    public Stream sourceFind(X s, X p, X o) {
+        return source.match(s,p,o);
+    }
+
+    @Override
+    protected boolean sourceContains(X s, X p, X o) {
+        return source.contains(s, p, o);
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchWrapper.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchWrapper.java
new file mode 100644
index 00000000000..b36f43f9798
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.rdfs.engine;
+
+import java.util.stream.Stream;
+
+public class MatchWrapper>
+    implements Match
+{
+    protected D delegate;
+
+    public MatchWrapper(D delegate) {
+        super();
+        this.delegate = delegate;
+    }
+
+    public D getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public Stream match(X s, X p, X o) {
+        return getDelegate().match(s, p, o);
+    }
+
+    @Override
+    public MapperX getMapper() {
+        return getDelegate().getMapper();
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java
index 56d94426fae..81e5408638c 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java
@@ -308,7 +308,7 @@ private void exec(String qs, Graph graph, Map> multimap1, Map void put(Map> multimap, X n1, X n2) {
         if ( !multimap.containsKey(n1) )
-            multimap.put(n1, new HashSet());
+            multimap.put(n1, new HashSet<>());
         multimap.get(n1).add(n2);
     }
 
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java
index 7e3eb4ac8ba..18bf0a083d1 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java
@@ -31,6 +31,8 @@
 
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.rdfs.engine.MapperX;
+import org.apache.jena.rdfs.engine.Mappers;
 import org.apache.jena.rdfs.engine.Match;
 
 /**
@@ -112,4 +114,9 @@ else if ( p.equals(rdfsRange) )
                 .stream()
                 .flatMap( e->e.getValue().stream().map(obj->Triple.create(e.getKey(), p, obj)) );
     }
+
+    @Override
+    public MapperX getMapper() {
+        return Mappers.mapperTriple();
+    }
 }
diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractDatasetGraphCompare.java b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractDatasetGraphCompare.java
new file mode 100644
index 00000000000..dc21b41c5e3
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractDatasetGraphCompare.java
@@ -0,0 +1,217 @@
+/*
+ * 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.rdfs;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.stream.IntStreams;
+import org.apache.commons.numbers.combinatorics.Combinations;
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.graph.NodeTransformLib;
+import org.junit.jupiter.api.DynamicTest;
+
+/**
+ * Test to check consistency of the find() method.
+ *
+ * Base class for generating tests that invoke the find() method of a dataset graph
+ * with all combinations of patterns.
+ */
+public abstract class AbstractDatasetGraphCompare {
+    private String testLabel;
+
+    public AbstractDatasetGraphCompare(String testLabel) {
+        super();
+        this.testLabel = testLabel;
+    }
+
+    public String getTestLabel() {
+        return testLabel;
+    }
+
+    /** By default consider cardinality when evaluating result sets. */
+    protected boolean defaultCompareAsSet() {
+        return true;
+    }
+
+
+    /** Sub classes can use this method to generate dynamic tests. */
+    public GraphFindTestBuilder prepareFindTests(DatasetGraph referenceDsg, DatasetGraph testDsg, DatasetGraph dataDsg) {
+        List findQuads = createFindQuads(dataDsg).toList();
+        return new GraphFindTestBuilder(testLabel, referenceDsg, testDsg, findQuads)
+            .compareAsSet(defaultCompareAsSet());
+    }
+
+    /** Derive a reference dataset by materalizing the dataset into a copy. */
+    public GraphFindTestBuilder prepareFindTests(DatasetGraph testDsg, DatasetGraph dataDsg) {
+        DatasetGraph referenceDsg = DatasetGraphFactory.create();
+        referenceDsg.addAll(testDsg);
+
+        List findQuads = createFindQuads(dataDsg).toList();
+        return new GraphFindTestBuilder(testLabel, referenceDsg, testDsg, findQuads)
+            .compareAsSet(defaultCompareAsSet());
+    }
+
+    /** Materialize the dataset as the reference dataset. Use reference dataset as dataDsg. */
+    public GraphFindTestBuilder prepareFindTests(DatasetGraph testDsg) {
+        DatasetGraph referenceDsg = DatasetGraphFactory.create();
+        referenceDsg.addAll(testDsg);
+        return prepareFindTests(testDsg, referenceDsg, testDsg)
+            .compareAsSet(defaultCompareAsSet());
+    }
+
+    // Markers for find-quad generation w.r.t. a dataset and a
+    // "meta pattern" such as (IN, foo, OUT, OUT).
+    // IN becomes substituted with concrete values, out becomes ANY.
+    private static final Node IN = NodeFactory.createBlankNode("IN");
+    private static final Node OUT = NodeFactory.createBlankNode("OUT");
+
+    /**
+     * Generate the set of find patterns for each quad in the source dataset.
+     * This is the set of combinations by substituting components with ANY.
+     * For example, the derivations for a concrete quad (g, s, p, o) are:
+     * 
+     * {(g, s, p, ANY), (g, s, ANY, o), (g, s, ANY, ANY), ...}
+     * 
+ */ + public static Stream createFindQuads(DatasetGraph dataSource) { + Node[] baseMetaPattern = new Node[]{OUT, OUT, OUT, OUT}; + Stream result = IntStream.rangeClosed(0, 4).boxed() + .flatMap(k -> Iter.asStream(Combinations.of(4, k).iterator())) + .flatMap(ins -> { + Node[] metaPattern = Arrays.copyOf(baseMetaPattern, baseMetaPattern.length); + + // Use IN to mark the components that we want to substitute with concrete values. + // OUT becomes ANY. + IntStreams.of(ins).forEach(i -> metaPattern[i] = IN); + Quad metaQuad = toQuad(List.of(metaPattern).iterator()); + + Set lookups = createFindQuads(dataSource, metaQuad); + return lookups.stream(); + }); + return result; + } + + private static Quad toQuad(Iterator it) { + Quad r = Quad.create(it.next(), it.next(), it.next(), it.next()); + if (it.hasNext()) { + throw new IllegalArgumentException("Iterator of exactly 4 elements expected."); + } + return r; + } + + private static Node outToAny(Node pattern, Node concrete) { + Node r = (OUT.equals(pattern)) ? Node.ANY : concrete; + return r; + } + + private static Node inToAny(Node node) { + Node r = (IN.equals(node)) ? Node.ANY : node; + return r; + } + + private static Node outToAny(Node node) { + Node r = (OUT.equals(node)) ? Node.ANY : node; + return r; + } + + private static Quad inToAny(Quad metaQuad) { + return NodeTransformLib.transform(AbstractDatasetGraphCompare::inToAny, metaQuad); + } + + private static Quad outToAny(Quad metaQuad) { + return NodeTransformLib.transform(AbstractDatasetGraphCompare::outToAny, metaQuad); + } + + // !!! This method implicitly gets rid of 'IN' !!! + // Components of the input quads are processed as follows: + // If a component of pattern is OUT then it becomes is ANY. + // Otherwise, *always* return the corresponding component of 'concrete'. + private static Quad createFindQuad(Quad meta, Quad concrete) { + Quad result = Quad.create( + outToAny(meta.getGraph(), concrete.getGraph()), + outToAny(meta.getSubject(), concrete.getSubject()), + outToAny(meta.getPredicate(), concrete.getPredicate()), + outToAny(meta.getObject(), concrete.getObject())); + return result; + } + + /** + * Expand a pattern such as (IN, s, OUT, OUT) into { (g1, s, ANY, ANY), (g2, s, ANY, ANY) } + * based on the concrete quads in dsg. + */ + private static Set createFindQuads(DatasetGraph dsg, Quad metaQuad) { + // Replace IN and OUT with ANY - this retains only term nodes. + Quad p = outToAny(inToAny(metaQuad)); + Set result = Iter.collect(Iter.map(dsg.find(p), q -> createFindQuad(metaQuad, q)), + Collectors.toCollection(LinkedHashSet::new)); + return result; + } + + /** + * Builder that accepts a reference dataset, a test dataset + * and a list of quads for which to produce {@link DynamicTest} instances. + * + *

+ * In addition, it allows to configure whether result sets should be compared as sets instead of lists. + * Useful to assess correctness in disregard of cardinality. + */ + public static class GraphFindTestBuilder { + private String testLabel; + private DatasetGraph referenceDsg; + private DatasetGraph testDsg; + private List findQuads; + private boolean compareAsSet; + + protected GraphFindTestBuilder(String testLabel, DatasetGraph referenceDsg, DatasetGraph testDsg, List findQuads) { + super(); + this.testLabel = Objects.requireNonNull(testLabel); + this.referenceDsg = Objects.requireNonNull(referenceDsg); + this.testDsg = Objects.requireNonNull(testDsg); + this.findQuads = findQuads; + } + + public List getFindQuads() { + return findQuads; + } + + public GraphFindTestBuilder compareAsSet(boolean onOrOff) { + this.compareAsSet = onOrOff; + return this; + } + + public List build() { + List tests = findQuads.stream().map(q -> DynamicTest.dynamicTest(testLabel + " " + q, + new GraphFindExecutable(testLabel, q, referenceDsg, testDsg, compareAsSet))).toList(); + return tests; + } + } +} diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Extra.java b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Extra.java new file mode 100644 index 00000000000..116e50553a0 --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Extra.java @@ -0,0 +1,85 @@ +/* + * 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.rdfs; + +import java.util.List; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +public abstract class AbstractTestRDFS_Extra + extends AbstractTestRDFS_Find +{ + public AbstractTestRDFS_Extra(String testLabel) { + super(testLabel); + } + + /** Some RDFS reasoners so far produce duplicates which fail cardinality tests. */ + @Override + protected boolean defaultCompareAsSet() { + return true; + } + + @TestFactory + @Disabled("Fails on certain patterns - Needs investigation.") + public List testSubPropertyOfRdfType01() { + List tests = prepareRdfsFindTestsSSE( + "(graph (:directType rdfs:subPropertyOf rdf:type) )", + "(graph (:fido :directType :Dog) )" + ).build(); + return tests; + } + + @TestFactory + public List testSubClassOf01() { + List tests = prepareRdfsFindTestsSSE( + "(graph (:Dog rdfs:subClassOf rdf:Mammal) )", + "(graph (:fido rdf:type :Dog) )" + ).build(); + return tests; + } + + @TestFactory + public List testRange01() { + List tests = prepareRdfsFindTestsSSE( + "(graph (:owner rdfs:range :Person) )", + "(graph (:fido :owner :alice) )" + ).build(); + return tests; + } + + @TestFactory + public List testRangeWithLiteral01() { + List tests = prepareRdfsFindTestsSSE( + "(graph (:name rdfs:range :Literal) )", + "(graph (:fido :name 'Fido') )" + ).build(); + return tests; + } + + @TestFactory + public List testDomain01() { + List tests = prepareRdfsFindTestsSSE( + "(graph (:owner rdfs:domain :Pet) )", + "(graph (:fido :owner :alice) )" + ).build(); + return tests; + } +} diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Find.java b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Find.java new file mode 100644 index 00000000000..21e0d9c015b --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Find.java @@ -0,0 +1,127 @@ +/* + * 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.rdfs; + +import java.util.List; +import java.util.stream.Stream; + +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.rdfs.setup.ConfigRDFS; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.apache.jena.sparql.core.Quad; +import org.apache.jena.sparql.sse.SSE; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; + +/** + * Test consistency of DatasetGraph.find(g, s, p, o) w.r.t. an RDFS setup. + *

+ * + * BEWARE: find() is used to produce the reference data. Errors in the reference data + * will likely cause otherwise correctly functioning tests to fail. + *

+ * + * (1) Materializes the result of dsg.find() into a reference dataset. + * (2) Invokes find(g, s, p, o) with all combinations and compares the results. + */ +public abstract class AbstractTestRDFS_Find + extends AbstractDatasetGraphCompare +{ + public AbstractTestRDFS_Find(String testLabel) { + super(testLabel); + } + + /** Some RDFS reasoners so far produce duplicates which fail cardinality tests. */ + @Override + protected boolean defaultCompareAsSet() { + return true; + } + + /** Sub classes need to implement this method and return a DatasetGraph with RDFS inferencing. */ + protected abstract DatasetGraph applyRdfs(DatasetGraph dsg, ConfigRDFS configRDFS); + + /** + * Prepare test cases. + * + * @param schemaStr SSE expression that parses as a {@link Graph}. + * @param dataStr SSE expression that parses as a {@link DatasetGraph}. + * @return A builder for the concrete test instances. + */ + public GraphFindTestBuilder prepareRdfsFindTestsSSE(String schemaStr, String dataStr) { + Graph graph = SSE.parseGraph(schemaStr); + DatasetGraph inputDsg = SSE.parseDatasetGraph(dataStr); + return prepareRdfsFindTests(graph, inputDsg); + } + + /** + * Prepare test cases. + * + * @param schemaStr RDF data in TRIG syntax that parses as a {@link Graph}. + * @param dataStr RDF data in TRIG syntax that parses as a {@link DatasetGraph}. + * @return A builder for the concrete test instances. + */ + public GraphFindTestBuilder prepareRdfsFindTestsTrig(String schemaStr, String dataStr) { + Graph schemaGraph = RDFParser.fromString(schemaStr, Lang.TTL).toGraph(); + SetupRDFS setup = RDFSFactory.setupRDFS(schemaGraph); + + DatasetGraph inputDsg = RDFParser.fromString(dataStr, Lang.TRIG).toDatasetGraph(); + return prepareRdfsFindTests(setup, inputDsg); + } + + public GraphFindTestBuilder prepareRdfsFindTests(Graph schemaGraph, DatasetGraph inputDsg) { + SetupRDFS setup = RDFSFactory.setupRDFS(schemaGraph); + return prepareRdfsFindTests(setup, inputDsg); + } + + public GraphFindTestBuilder prepareRdfsFindTests(ConfigRDFS configRDFS, DatasetGraph inputDsg) { + DatasetGraph testDsg = applyRdfs(inputDsg, configRDFS); + + // Build reference data by materializing a copy of testDsg via findAll(). + DatasetGraph referenceDsg = DatasetGraphFactory.create(); + referenceDsg.prefixes().putAll(testDsg.prefixes()); + referenceDsg.addAll(testDsg); + + // dataDsg that is the source for substituting placeholders + // in the quads that will be used to test find(quad) calls. + DatasetGraph dataDsg = DatasetGraphFactory.create(); + dataDsg.prefixes().putAll(referenceDsg.prefixes()); + dataDsg.addAll(referenceDsg); + + boolean debugPrintReferenceData = false; + if (debugPrintReferenceData) { + RDFDataMgr.write(System.out, referenceDsg, RDFFormat.TRIG_PRETTY); + } + + // Add all (non-literal) objects to the source Data for more extensive testing of lookups. + try (Stream stream = dataDsg.stream()) { + List extra = stream.flatMap(q -> Stream.of(q.getPredicate(), q.getObject()) + .map(x -> Quad.create(q.getGraph(), x, RDF.Nodes.type, RDFS.Nodes.Resource))) + .toList(); + extra.forEach(dataDsg::add); + } + + return prepareFindTests(referenceDsg, testDsg, referenceDsg); + } +} diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/GraphFindExecutable.java b/jena-arq/src/test/java/org/apache/jena/rdfs/GraphFindExecutable.java new file mode 100644 index 00000000000..304ceebd90f --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/GraphFindExecutable.java @@ -0,0 +1,99 @@ +/* + * 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.rdfs; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.jena.atlas.iterator.Iter; +import org.apache.jena.atlas.lib.ListUtils; +import org.apache.jena.riot.out.NodeFmtLib; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.Quad; +import org.junit.jupiter.api.function.Executable; + +/** + * JUnit test case executable to compare two datasets w.r.t. + * their result of find(pattern). + */ +public class GraphFindExecutable + implements Executable +{ + private static PrintStream out = System.err; + + private String testLabel; + private Quad findPattern; + private DatasetGraph referenceDsg; + private DatasetGraph testDsg; + + /** Compare expected and actual data as sets instead of lists. */ + private boolean compareAsSet; + + public GraphFindExecutable(String testLabel, Quad findPattern, DatasetGraph referenceDsg, DatasetGraph testDsg, boolean compareAsSet) { + super(); + this.testLabel = testLabel; + this.referenceDsg = referenceDsg; + this.testDsg = testDsg; + this.findPattern = findPattern; + this.compareAsSet = compareAsSet; + } + + public String getTestLabel() { + return testLabel; + } + + public Quad getFindPattern() { + return findPattern; + } + + /** + * Assert that graph.find() returned the same set of quads for the given pattern. + * Duplicates are ignored. + */ + @Override + public void execute() throws Throwable { + List expectedList = Iter.toList(referenceDsg.find(findPattern)); + List actualList = Iter.toList(testDsg.find(findPattern)); + + if (compareAsSet) { + Set expectedSet = new LinkedHashSet<>(expectedList); + Set actualSet = new LinkedHashSet<>(actualList); + expectedList = new ArrayList<>(expectedSet); + actualList = new ArrayList<>(actualSet); + } + + boolean b = ListUtils.equalsUnordered(expectedList, actualList); + if ( ! b ) { + out.println("Fail: find(" + NodeFmtLib.str(findPattern) + ")"); + LibTestRDFS.printDiff(out, expectedList, actualList); + } + + assertTrue(b,()->getTestLabel()); + } + + @Override + public String toString() { + return getTestLabel() + " " + getFindPattern(); + } +} diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java b/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java index 83419bd6f01..6a957a15972 100644 --- a/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java @@ -51,6 +51,8 @@ , TestInfSPARQL.class , TestAssemblerRDFS.class + + , TestDatasetGraphFindRDFS.class }) public class TS_InfRdfs { } diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphFindRDFS.java b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphFindRDFS.java new file mode 100644 index 00000000000..716c403ae3d --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphFindRDFS.java @@ -0,0 +1,41 @@ +/* + * 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.rdfs; + +import org.apache.jena.graph.Node; +import org.apache.jena.rdfs.setup.ConfigRDFS; +import org.apache.jena.sparql.core.DatasetGraph; + +public class TestDatasetGraphFindRDFS + extends AbstractTestRDFS_Extra +{ + public TestDatasetGraphFindRDFS() { + super("RDFS"); + } + + @Override + protected boolean defaultCompareAsSet() { + return true; + } + + @Override + protected DatasetGraph applyRdfs(DatasetGraph dsg, ConfigRDFS configRDFS) { + return new DatasetGraphRDFS(dsg, (SetupRDFS)configRDFS); + } +} diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java index 095a366c3e3..4843851a177 100644 --- a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java @@ -42,6 +42,7 @@ import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphWrapper; import org.apache.jena.sparql.core.Quad; import org.apache.jena.sparql.sse.SSE; @@ -54,7 +55,7 @@ public class TestDatasetGraphRDFS { private static PrintStream out = System.out; - private static DatasetGraphRDFS dsg; + private static DatasetGraphWrapper dsg; @BeforeAll public static void beforeClass() { @@ -66,7 +67,7 @@ public static void beforeClass() { ); DatasetGraph dsgBase = SSE.parseDatasetGraph(x); Graph schema = SSE.parseGraph("(graph (:A rdfs:subClassOf :B))"); - dsg = (DatasetGraphRDFS)RDFSFactory.datasetRDFS(dsgBase, schema); + dsg = (DatasetGraphWrapper)RDFSFactory.datasetRDFS(dsgBase, schema); } @Test public void dsg_access_1() { diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java index 96dfa5e029e..b9015a7e889 100644 --- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java +++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java @@ -29,5 +29,4 @@ public class FilterUnique implements Predicate { public boolean test(T item) { return seen.add(item); } - } diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUniqueCache.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUniqueCache.java new file mode 100644 index 00000000000..886865a4be4 --- /dev/null +++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUniqueCache.java @@ -0,0 +1,40 @@ +/* + * 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.atlas.iterator; + +import java.util.function.Predicate; + +import org.apache.jena.atlas.lib.Cache; +import org.apache.jena.atlas.lib.CacheFactory; + +public class FilterUniqueCache implements Predicate { + private final Cache seen; + + public FilterUniqueCache(int size) { + super(); + this.seen = CacheFactory.createCache(size); + } + + @Override + public boolean test(T item) { + boolean wasSeen = seen.containsKey(item); + seen.put(item, Boolean.TRUE); + return !wasSeen; + } +} diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java index e72c3d46085..4db202f60ab 100644 --- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java +++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java @@ -27,6 +27,7 @@ import java.util.stream.StreamSupport; import org.apache.jena.atlas.io.IO; +import org.apache.jena.atlas.lib.CacheFactory; import org.apache.jena.atlas.lib.Closeable; import org.apache.jena.atlas.lib.Sink; @@ -113,6 +114,10 @@ public static Iter ofNullable(T t) { return t == null ? Iter.empty() : Iter.of(t); } + public static Iterator ofStream(Stream stream) { + return Iter.onClose(stream.iterator(), stream::close); + } + /** * Return an iterator that does not permit remove. * This makes an "UnmodifiableIterator". @@ -530,6 +535,14 @@ public static Iterator distinct(Iterator iter) { return filter(iter, new FilterUnique()); } + /** Returns an iterator that uses an LRU cache to filter out duplicates. + * This provides a best-effort distinct view within a sliding window of recent elements. + * Memory usage is bounded by the specified cache size. + */ + public static Iterator distinctCached(Iterator iter, int cacheMaxSize) { + return filter(iter, new FilterUniqueCache(cacheMaxSize)); + } + /** Remove adjacent duplicates. This operation does not need * working memory to remember the all elements already seen, * just a slot for the last element seen. diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java index 04b82cfd9cf..5d251639686 100644 --- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java +++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java @@ -114,11 +114,11 @@ public void forEachRemaining(Consumer action) { @Override public void close() { //iterators.forEach(Iter::close); - // Earlier iterators already closed + // Earlier iterators already closed. Handle case where hasNext has never been called. + if (idx == -1) idx = 0; for ( int i = idx ; i < iterators.size() ; i++ ) { Iterator iter = iterators.get(idx); Iter.close(iter); } - } } diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MapperXTDB.java b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MapperXTDB.java new file mode 100644 index 00000000000..067520f05cf --- /dev/null +++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MapperXTDB.java @@ -0,0 +1,57 @@ +/* + * 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.tdb2.match; + +import org.apache.jena.atlas.lib.tuple.Tuple3; +import org.apache.jena.atlas.lib.tuple.TupleFactory; +import org.apache.jena.graph.Node; +import org.apache.jena.rdfs.engine.MapperX; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.tdb2.store.DatasetGraphTDB; +import org.apache.jena.tdb2.store.NodeId; +import org.apache.jena.tdb2.store.nodetable.NodeTable; +import org.apache.jena.tdb2.sys.TDBInternal; + +public class MapperXTDB + implements MapperX> +{ + private NodeTable nodeTable; + + protected MapperXTDB(NodeTable nodeTable) { + super(); + this.nodeTable = nodeTable; + } + + public static MapperX> create(DatasetGraph dsg) { + DatasetGraphTDB tdb = TDBInternal.getDatasetGraphTDB(dsg); + if (tdb == null) { + throw new IllegalArgumentException("Argument must be a TDB2 dataset graph."); + } + return new MapperXTDB(tdb.getQuadTable().getNodeTupleTable().getNodeTable()); + } + + @Override public NodeId fromNode(Node n) { return nodeTable.getNodeIdForNode(n); } + @Override public Node toNode (NodeId x) { return nodeTable.getNodeForNodeId(x); } + + @Override public NodeId subject (Tuple3 tuple) { return tuple.get(0); } + @Override public NodeId predicate(Tuple3 tuple) { return tuple.get(1); } + @Override public NodeId object (Tuple3 tuple) { return tuple.get(2); } + + @Override public Tuple3 tuple(NodeId s, NodeId p, NodeId o) { return TupleFactory.create3(s, p, o); } +} diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MatchTDB.java b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MatchTDB.java new file mode 100644 index 00000000000..2aac132b691 --- /dev/null +++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MatchTDB.java @@ -0,0 +1,74 @@ +/* + * 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.tdb2.match; + +import java.util.stream.Stream; + +import org.apache.jena.atlas.iterator.Iter; +import org.apache.jena.atlas.lib.tuple.Tuple3; +import org.apache.jena.atlas.lib.tuple.TupleFactory; +import org.apache.jena.graph.Graph; +import org.apache.jena.rdfs.engine.MapperX; +import org.apache.jena.rdfs.engine.Match; +import org.apache.jena.tdb2.store.GraphTDB; +import org.apache.jena.tdb2.store.GraphViewSwitchable; +import org.apache.jena.tdb2.store.NodeId; +import org.apache.jena.tdb2.store.nodetable.NodeTable; + +public class MatchTDB + implements Match> +{ + private GraphTDB graph; + private MapperX> mapper; + + protected MatchTDB(GraphTDB graph, MapperX> mapper) { + super(); + this.graph = graph; + this.mapper = mapper; + } + + public static MatchTDB wrap(Graph g) { + // Same pattern used in stage generator - move to TDBInternal? + if ( g instanceof GraphViewSwitchable gvs ) + g = gvs.getBaseGraph(); + + if (!(g instanceof GraphTDB tdbGraph)) { + throw new IllegalArgumentException("Not a TDB2 graph"); + } + + NodeTable nodeTable = tdbGraph.getNodeTupleTable().getNodeTable(); + MapperX> mapper = new MapperXTDB(nodeTable); + return new MatchTDB(tdbGraph, mapper); + } + + @Override + public Stream> match(NodeId s, NodeId p, NodeId o) { + return Iter.asStream(graph.getNodeTupleTable().find(s, p, o)) + .filter(t -> { + boolean b = NodeId.isDoesNotExist(t.get(0)) || NodeId.isDoesNotExist(t.get(1)) || NodeId.isDoesNotExist(t.get(2)); + return !b; + }) + .map(t -> TupleFactory.create3(t.get(0), t.get(1), t.get(2))); + } + + @Override + public MapperX> getMapper() { + return mapper; + } +} diff --git a/jena-tdb2/src/test/java/org/apache/jena/tdb2/match/TestMatchTDB2.java b/jena-tdb2/src/test/java/org/apache/jena/tdb2/match/TestMatchTDB2.java new file mode 100644 index 00000000000..a77091a55a3 --- /dev/null +++ b/jena-tdb2/src/test/java/org/apache/jena/tdb2/match/TestMatchTDB2.java @@ -0,0 +1,82 @@ +/* + * 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.tdb2.match; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.apache.jena.atlas.iterator.Iter; +import org.apache.jena.atlas.lib.tuple.Tuple3; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Triple; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.rdfs.RDFSFactory; +import org.apache.jena.rdfs.engine.DatasetGraphWithGraphTransform; +import org.apache.jena.rdfs.engine.GraphMatch; +import org.apache.jena.rdfs.engine.MapperX; +import org.apache.jena.rdfs.engine.MatchRDFSWrapper; +import org.apache.jena.rdfs.setup.ConfigRDFS; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.sse.SSE; +import org.apache.jena.system.AutoTxn; +import org.apache.jena.system.G; +import org.apache.jena.system.Txn; +import org.apache.jena.tdb2.TDB2Factory; +import org.apache.jena.tdb2.store.NodeId; +import org.junit.jupiter.api.Test; + +public class TestMatchTDB2 { + @Test + public void testRdfsOnNodeIdLevel() { + Graph schema = SSE.parseGraph("(graph (rdf:type rdf:type rdf:Property) (:p rdfs:domain :C) )"); + + Dataset baseDs = TDB2Factory.createDataset(); + DatasetGraph baseDsg = baseDs.asDatasetGraph(); + + MapperX> mapper = MapperXTDB.create(baseDsg); + + try (AutoTxn txn = Txn.autoTxn(baseDsg, ReadWrite.WRITE)) { + // !!! The schema must be added first, otherwise we won't have NodeIds !!! + // !!! Also, all terms (especially rdf:type) must have corresponding NodeIds !!! + G.addInto(baseDsg.getDefaultGraph(), schema); + ConfigRDFS configRDFS = RDFSFactory.setupRDFS(schema, mapper); + + // Add wrapping on NodeId level. + DatasetGraph rdfsDsg = new DatasetGraphWithGraphTransform(baseDsg, + g -> GraphMatch.adapt(g, new MatchRDFSWrapper<>(configRDFS, MatchTDB.wrap(g)))); + + // Add data. + Graph data = SSE.parseGraph("(graph (:s :p :o) )"); + G.addInto(rdfsDsg.getDefaultGraph(), data); + + // Execute queries and compare. + Graph expectedGraph = SSE.parseGraph("(graph (:s rdf:type :C) )"); + Graph actualGraph = QueryExec.dataset(rdfsDsg).query("CONSTRUCT WHERE { a ?o }").construct(); + + Set expected = Iter.toSet(expectedGraph.find()); + Set actual = Iter.toSet(actualGraph.find()); + + assertEquals(expected, actual); + txn.commit(); + } + } +} diff --git a/pom.xml b/pom.xml index 19e673ce6a2..091234ec33a 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,7 @@ 2.20.0 1.7.0 3.19.0 + 4.0-beta1 0.5.0 1.14.1 1.19.0 @@ -104,7 +105,7 @@ 4.13.2 6.0.0 - + 5.20.0 4.3.0 @@ -421,6 +422,12 @@ ${ver.commons-lang3} + + org.apache.commons + commons-math4-legacy + ${ver.commons-math4} + + org.apache.commons commons-collections4 @@ -948,7 +955,7 @@ ${java.version} -proc:none -