Skip to content

Commit b8e472d

Browse files
namedgraphclaude
andcommitted
Fix ScopedGraphRepository.contains() dropping resolvable imports
ontapi consults GraphRepository.contains() before get() when resolving an ontology's imports closure. PrefixGraphRepository.contains() reports cache state (loaded graphs), not resolvability, so every import that resolves through a bundled location mapping (dh, sp, spin, foaf, sioc, sd), SPARQL-first loading or HTTP was answered with false on first resolution — and ontapi silently substituted an empty ontology graph for it (its ignoreUnresolvedImports fallback). The closure kept its shape but lost the content of every such import: SPIN constraints vanished, so validation enforced nothing (422 tests wrote through, eventually applying invalid dataspace settings and cascading into NPEs), and vocabulary term lookups came up empty. This is what failed the HTTP test suite in CI. contains() now attempts resolution through the backing repository (which loads and caches the graph) after the cache checks, reporting absent only for genuinely unresolvable ids. Adds a production-shaped regression test: ns# ontology importing the SPARQL-seeded ldh# vocabulary with its transitive imports resolved through the real bundled location mappings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 32d5ec2 commit b8e472d

2 files changed

Lines changed: 116 additions & 1 deletion

File tree

src/main/java/com/atomgraph/linkeddatahub/server/util/ScopedGraphRepository.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,21 @@ public void clear()
9191
@Override
9292
public boolean contains(String id)
9393
{
94-
return local.containsKey(id) || getBacking().contains(id);
94+
if (local.containsKey(id) || getBacking().contains(id)) return true;
95+
96+
// the backing repository's contains() only reports already-cached graphs, but ontapi consults
97+
// contains() before get() when resolving imports — a false negative for a resolvable id (bundled
98+
// mapping, SPARQL-first, HTTP) makes ontapi silently substitute an empty ontology graph for the
99+
// import. Attempt resolution instead: the backing repository loads and caches the graph, and only
100+
// a genuinely unresolvable id reports absent
101+
try
102+
{
103+
return getBacking().get(id) != null;
104+
}
105+
catch (RuntimeException ex)
106+
{
107+
return false;
108+
}
95109
}
96110

97111
@Override
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright 2026 Martynas Jusevičius <martynas@atomgraph.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.atomgraph.linkeddatahub.server.filter.request;
18+
19+
import com.atomgraph.client.util.jena.PrefixGraphRepository;
20+
import org.apache.jena.ontapi.UnionGraph;
21+
import org.apache.jena.rdf.model.Model;
22+
import org.apache.jena.rdf.model.ModelFactory;
23+
import org.apache.jena.rdf.model.Resource;
24+
import org.apache.jena.rdf.model.ResourceFactory;
25+
import org.apache.jena.riot.RDFParser;
26+
import org.apache.jena.vocabulary.OWL;
27+
import org.apache.jena.vocabulary.RDF;
28+
import org.apache.jena.vocabulary.RDFS;
29+
import org.junit.jupiter.api.Test;
30+
import static org.junit.jupiter.api.Assertions.*;
31+
32+
/**
33+
* Production-shaped regression guard for the imports closure: the end-user ns# ontology as the SPARQL
34+
* CONSTRUCT returns it (header + imports + unrelated document resources), importing the SPARQL-loaded
35+
* ldh# vocabulary, whose transitive imports resolve through the real bundled location mappings
36+
* (dh, spin, sp, foaf, sioc, sd) plus store-seeded stubs for the non-mapped ones (ac, nfo, owl).
37+
* <p>
38+
* Pins the fix for ontapi consulting {@code contains()} before {@code get()} during import resolution:
39+
* a cache-state (rather than resolvability) answer made ontapi silently substitute empty ontology
40+
* graphs for every bundled-mapped import, stripping SPIN constraints and vocabularies from the closure.
41+
*
42+
* @author Martynas Jusevičius {@literal <martynas@atomgraph.com>}
43+
*/
44+
public class OntologyClosureCIReproTest
45+
{
46+
47+
private static final String NS = "https://localhost:4443/ns#";
48+
private static final String LDH = "https://w3id.org/atomgraph/linkeddatahub#";
49+
50+
@Test
51+
public void productionShapedClosureContainsAllImports()
52+
{
53+
PrefixGraphRepository repository = new PrefixGraphRepository(null);
54+
55+
// real bundled mappings
56+
Model mappingModel = ModelFactory.createDefaultModel();
57+
RDFParser.create().source("location-mapping.ttl").streamManager(repository.getStreamManager()).build().parse(mappingModel);
58+
repository.processConfig(mappingModel);
59+
60+
// mimic SPARQL-first load result: the ldh# vocabulary as a store graph
61+
Model ldh = ModelFactory.createDefaultModel();
62+
RDFParser.create().source("com/atomgraph/linkeddatahub/ldh.ttl").base(LDH).streamManager(repository.getStreamManager()).build().parse(ldh);
63+
repository.put(LDH, ldh.getGraph());
64+
65+
// stub graphs for ldh#'s non-mapped imports (SPARQL/HTTP-loaded in production)
66+
for (String stub : new String[] {
67+
"https://w3id.org/atomgraph/client#",
68+
"http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#",
69+
"http://www.w3.org/2002/07/owl#" })
70+
{
71+
Model m = ModelFactory.createDefaultModel();
72+
m.add(m.createResource(stub), RDF.type, OWL.Ontology);
73+
repository.put(stub, m.getGraph());
74+
}
75+
76+
// ns# base graph as the ontology CONSTRUCT returns it: ontology header + imports + document resource
77+
Model ns = ModelFactory.createDefaultModel();
78+
Resource nsOnt = ns.createResource(NS);
79+
ns.add(nsOnt, RDF.type, OWL.Ontology);
80+
ns.add(nsOnt, OWL.imports, ns.createResource(LDH));
81+
Resource doc = ns.createResource("https://admin.localhost:4443/ontologies/namespace/");
82+
ns.add(doc, RDF.type, ns.createResource("https://www.w3.org/ns/ldt/document-hierarchy#Item"));
83+
ns.add(doc, ResourceFactory.createProperty("http://purl.org/dc/terms/title"), "Namespace");
84+
repository.put(NS, ns.getGraph());
85+
86+
UnionGraph union = OntologyFilter.loadOntology(repository, NS);
87+
Model closure = ModelFactory.createModelForGraph(union);
88+
89+
// direct import: ldh.ttl content
90+
assertTrue(closure.contains(closure.createResource(LDH + "View"), RDF.type, RDFS.Class), "ldh# (direct import) must be in the closure");
91+
// transitive via ldh#: dh.ttl (bundled mapping)
92+
assertTrue(closure.contains(closure.createResource("https://www.w3.org/ns/ldt/document-hierarchy#Item"), RDF.type, OWL.Class), "dh# (transitive, bundled) must be in the closure");
93+
// transitive via ldh#: spin.ttl imported as http://spinrdf.org/spin (no hash)
94+
assertTrue(closure.contains(closure.createResource("http://spinrdf.org/spin#constraint"), RDF.type, RDF.Property), "spin (transitive, bundled, hashless import URI) must be in the closure");
95+
// transitive via dh#: sp.ttl imported as http://spinrdf.org/sp#
96+
assertTrue(closure.contains(closure.createResource("http://spinrdf.org/sp#text"), RDF.type, RDF.Property), "sp# (transitive via dh#, bundled) must be in the closure");
97+
// transitive via dh#: foaf (bundled)
98+
assertFalse(closure.listStatements(closure.createResource("http://xmlns.com/foaf/0.1/Agent"), null, (org.apache.jena.rdf.model.RDFNode)null).toList().isEmpty(), "foaf (transitive, bundled) must be in the closure");
99+
}
100+
101+
}

0 commit comments

Comments
 (0)