From edda093005c0513b97dc56e0fb1d873b2e1fd2fb Mon Sep 17 00:00:00 2001 From: Ryan Shaw Date: Wed, 7 Oct 2026 23:44:19 +0000 Subject: [PATCH] GH-4312: RDFS: subPropertyOf without subClassOf; super-property domain and range ApplyRDFS.subProperty() short-circuited on hasClassDeclarations(), so a vocabulary with rdfs:subPropertyOf but no rdfs:subClassOf inferred nothing from it. It now tests hasPropertyDeclarations(). The domain and range of a super-property were never applied to triples using a sub-property: (:x :p :y), (:p rdfs:subPropertyOf :q), (:q rdfs:domain :D) did not give (:x rdf:type :D), in either the stream (ApplyRDFS) or the graph (MatchRDFS). CxtInf now has lookups that include super-properties (domain, range) and sub-properties (properties by domain, by range), used by both. Generated-by: Claude Opus 5.5 (Claude Code) Co-Authored-By: Claude Opus 5.5 --- .../apache/jena/rdfs/engine/ApplyRDFS.java | 6 +- .../org/apache/jena/rdfs/engine/CxtInf.java | 44 ++++++++++ .../apache/jena/rdfs/engine/MatchRDFS.java | 8 +- .../org/apache/jena/rdfs/TestMiscRDFS.java | 25 ++++++ .../org/apache/jena/rdfs/TestStreamRDFS.java | 86 +++++++++++++++++++ 5 files changed, 162 insertions(+), 7 deletions(-) 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 323d7c7d10c..dc9c1f160ae 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 @@ -100,7 +100,7 @@ private void subClass(X s, X p, X o, Output out) { * [rdfs6: (?a ?p ?b), (?p rdfs:subPropertyOf ?q) -> (?a ?q ?b)] */ private void subProperty(X s, X p, X o, Output out) { - if ( SHORT_CIRCUIT && ! setup.hasClassDeclarations() ) + if ( SHORT_CIRCUIT && ! setup.hasPropertyDeclarations() ) return; Set x = setup.getSuperProperties(p); x.forEach(p2 -> derive(s, p2, o, out)); @@ -115,7 +115,7 @@ final private void domain(X s, X p, X o, Output out) { if ( ! setup.hasDomainDeclarations() ) return; } - Set x = setup.getDomain(p); + Set x = getDomainInc(p); x.forEach(c -> { derive(s, rdfType, c, out); subClass(s, rdfType, c, out); @@ -131,7 +131,7 @@ final private void range(X s, X p, X o, Output out) { if ( ! setup.hasRangeDeclarations() ) return; } - Set x = setup.getRange(p); + Set x = getRangeInc(p); if (!mapper.isLiteral(o)) { x.forEach(c -> { derive(o, rdfType, c, out); diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/CxtInf.java b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/CxtInf.java index 24b0b11d887..8aca873738e 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/CxtInf.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/CxtInf.java @@ -21,7 +21,10 @@ package org.apache.jena.rdfs.engine; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import org.apache.jena.graph.Node; import org.apache.jena.rdfs.setup.ConfigRDFS; @@ -64,4 +67,45 @@ protected boolean isANY(X x) { protected boolean isTerm(X x) { return !isANY(x); } + + // A triple using a property also uses each of its super-properties + // (rdfs:subPropertyOf), so the domains and ranges of the super-properties + // apply to it as well. + + /** The domains of a property and of its super-properties. */ + protected Set getDomainInc(X property) { + return withSuperProperties(property, setup::getDomain); + } + + /** The ranges of a property and of its super-properties. */ + protected Set getRangeInc(X property) { + return withSuperProperties(property, setup::getRange); + } + + /** The properties with a given domain, and their sub-properties. */ + protected Set getPropertiesByDomainInc(X type) { + return withSubProperties(setup.getPropertiesByDomain(type)); + } + + /** The properties with a given range, and their sub-properties. */ + protected Set getPropertiesByRangeInc(X type) { + return withSubProperties(setup.getPropertiesByRange(type)); + } + + private Set withSuperProperties(X property, Function> lookup) { + Set superProperties = setup.getSuperProperties(property); + if ( superProperties.isEmpty() ) + return lookup.apply(property); + Set acc = new HashSet<>(lookup.apply(property)); + superProperties.forEach(p -> acc.addAll(lookup.apply(p))); + return acc; + } + + private Set withSubProperties(Set properties) { + if ( properties.isEmpty() || ! setup.hasPropertyDeclarations() ) + return properties; + Set acc = new HashSet<>(properties); + properties.forEach(p -> acc.addAll(setup.getSubProperties(p))); + return acc; + } } 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 872051b91dd..b4d536b09eb 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 @@ -376,7 +376,7 @@ private void accInstances(Set tuples, Set types, X requestedType) { private void accInstancesDomain(Set tuples, Set types, X requestedType) { for ( X type : types ) { - Set predicates = setup.getPropertiesByDomain(type); + Set predicates = getPropertiesByDomainInc(type); if ( isEmpty(predicates) ) continue; predicates.forEach(p -> { @@ -388,7 +388,7 @@ private void accInstancesDomain(Set tuples, Set types, X requestedType) { private void accInstancesRange(Set tuples, Set types, X requestedType) { for ( X type : types ) { - Set predicates = setup.getPropertiesByRange(type); + Set predicates = getPropertiesByRangeInc(type); if ( isEmpty(predicates) ) continue; predicates.forEach(p -> { @@ -407,7 +407,7 @@ private void accTypesDomain(Set types, X X) { Stream stream = sourceFind(X, ANY, ANY); stream.forEach(triple -> { X p = predicate(triple); - Set x = setup.getDomain(p); + Set x = getDomainInc(p); types.addAll(x); }); } @@ -416,7 +416,7 @@ private void accTypesRange(Set types, X X) { Stream stream = sourceFind(ANY, ANY, X); stream.forEach(triple -> { X p = predicate(triple); - Set x = setup.getRange(p); + Set x = getRangeInc(p); types.addAll(x); }); } diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TestMiscRDFS.java b/jena-arq/src/test/java/org/apache/jena/rdfs/TestMiscRDFS.java index b84351052b4..20e8bc1d5ef 100644 --- a/jena-arq/src/test/java/org/apache/jena/rdfs/TestMiscRDFS.java +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestMiscRDFS.java @@ -92,6 +92,31 @@ public class TestMiscRDFS { assertEquals(5, count(rdfs, ANY, ANY, ANY)); } + // The domain of a super-property, with no rdfs:subClassOf in the vocabulary. + @Test public void subPropertyDomain() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q) (:q rdfs:domain :D) )"); + Graph data = SSE.parseGraph("(graph (:x :p :y))"); + Graph rdfs = RDFSFactory.graphRDFS(data, vocab); + exactlyOne(rdfs, node(":x"), node(":q"), node(":y")); + exactlyOne(rdfs, node(":x"), rdfType, node(":D")); + exactlyOne(rdfs, node(":x"), rdfType, ANY); + exactlyOne(rdfs, ANY, rdfType, node(":D")); + exactlyOne(rdfs, ANY, rdfType, ANY); + assertEquals(3, count(rdfs, ANY, ANY, ANY)); + assertEquals(3, count(rdfs, node(":x"), ANY, ANY)); + } + + // The range of a super-property, with no rdfs:subClassOf in the vocabulary. + @Test public void subPropertyRange() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q) (:q rdfs:range :R) )"); + Graph data = SSE.parseGraph("(graph (:x :p :y))"); + Graph rdfs = RDFSFactory.graphRDFS(data, vocab); + exactlyOne(rdfs, node(":y"), rdfType, node(":R")); + exactlyOne(rdfs, node(":y"), rdfType, ANY); + exactlyOne(rdfs, ANY, rdfType, node(":R")); + assertEquals(3, count(rdfs, ANY, ANY, ANY)); + } + @Test public void dataset() { Graph vocab = SSE.parseGraph("(graph (:S rdfs:subClassOf :T))"); DatasetGraph dsg0 = DatasetGraphFactory.createTxnMem(); diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TestStreamRDFS.java b/jena-arq/src/test/java/org/apache/jena/rdfs/TestStreamRDFS.java index 2bbbd194f8d..e8e7de845f0 100644 --- a/jena-arq/src/test/java/org/apache/jena/rdfs/TestStreamRDFS.java +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestStreamRDFS.java @@ -98,6 +98,83 @@ public class TestStreamRDFS { assertTrue(listContains(results, "(:X rdf:type :U)")); } + // A sub-property of a property with a domain, in the shared test vocabulary. + @Test public void infer_3() { + Triple t = SSE.parseTriple("(:c :pp :x)"); + List results = infOutput(x->x.triple(t)); + + Set resultSet = new HashSet<>(results); + assertEquals(5, resultSet.size()); + assertTrue(listContains(results, "(:c :pp :x)")); + assertTrue(listContains(results, "(:c :p :x)")); + assertTrue(listContains(results, "(:c :pTop :x)")); + assertTrue(listContains(results, "(:c rdf:type :Q)")); + assertTrue(listContains(results, "(:c rdf:type :Q2)")); + } + + // rdfs:subPropertyOf in a vocabulary with no rdfs:subClassOf. + @Test public void subProperty_1() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q))"); + List results = infOutput(vocab, "(:x :p :y)"); + + assertEquals(2, results.size()); + assertTrue(listContains(results, "(:x :p :y)")); + assertTrue(listContains(results, "(:x :q :y)")); + } + + @Test public void subProperty_2() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q) (:q rdfs:subPropertyOf :r))"); + List results = infOutput(vocab, "(:x :p :y)"); + + assertEquals(3, results.size()); + assertTrue(listContains(results, "(:x :q :y)")); + assertTrue(listContains(results, "(:x :r :y)")); + } + + // The domain of a super-property. + @Test public void subPropertyDomain_1() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q) (:q rdfs:domain :D))"); + List results = infOutput(vocab, "(:x :p :y)"); + + assertEquals(3, results.size()); + assertTrue(listContains(results, "(:x :q :y)")); + assertTrue(listContains(results, "(:x rdf:type :D)")); + } + + // The range of a super-property. + @Test public void subPropertyRange_1() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q) (:q rdfs:range :R))"); + List results = infOutput(vocab, "(:x :p :y)"); + + assertEquals(3, results.size()); + assertTrue(listContains(results, "(:x :q :y)")); + assertTrue(listContains(results, "(:y rdf:type :R)")); + } + + // The domain of a super-property of a super-property, then its super-class. + @Test public void subPropertyDomain_2() { + Graph vocab = SSE.parseGraph(""" + (graph (:p rdfs:subPropertyOf :q) (:q rdfs:subPropertyOf :r) + (:r rdfs:domain :D) (:D rdfs:subClassOf :E)) + """); + List results = infOutput(vocab, "(:x :p :y)"); + + assertEquals(5, results.size()); + assertTrue(listContains(results, "(:x :q :y)")); + assertTrue(listContains(results, "(:x :r :y)")); + assertTrue(listContains(results, "(:x rdf:type :D)")); + assertTrue(listContains(results, "(:x rdf:type :E)")); + } + + // The same domain on a property and its super-property is inferred once. + @Test public void subPropertyDomain_3() { + Graph vocab = SSE.parseGraph("(graph (:p rdfs:subPropertyOf :q) (:p rdfs:domain :D) (:q rdfs:domain :D))"); + List results = infOutput(vocab, "(:x :p :y)"); + + assertEquals(3, results.size()); + assertTrue(listContains(results, "(:x rdf:type :D)")); + } + private static boolean listContains(List list, String strTriple) { Triple triple = SSE.parseTriple(strTriple); return list.stream().anyMatch(t->triple.equals(t)); @@ -115,6 +192,15 @@ private static List infOutput(Consumer action) { return dest.getTriples(); } + // Inference of one triple, with a given vocabulary, to a list + private static List infOutput(Graph vocabulary, String strTriple) { + Triple t = SSE.parseTriple(strTriple); + CollectorStreamRDF dest = new CollectorStreamRDF(); + StreamRDF stream = RDFSFactory.streamRDFS(dest, vocabulary); + exec(stream, x->x.triple(t)); + return dest.getTriples(); + } + private static void exec(StreamRDF stream, Consumer action) { stream.start(); try {