From 4b6bd5b59eaa57e0a804a6f01cbc8159c6e73ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 13:36:19 +0200 Subject: [PATCH 1/9] Migrate vocabularies to Jena ontapi; add PrefixGraphRepository - Add jena-ontapi:6.1.0 dependency - Migrate SD, A vocabularies off the deprecated-for-removal org.apache.jena.ontology API to org.apache.jena.ontapi (OntModelFactory/OntSpecification, createOntClass/createDataProperty) - Add PrefixGraphRepository (extends DocumentGraphRepository): exact + longest-prefix URI->location mapping, HTTP loading via GraphStoreClient and classpath/file via RIOT StreamManager; replaces the DataManagerImpl loader, PrefixMapper and ontology ModelGetter - Add PrefixGraphRepositoryTest Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 6 + .../core/util/jena/PrefixGraphRepository.java | 219 ++++++++++++++++++ .../java/com/atomgraph/core/vocabulary/A.java | 37 +-- .../com/atomgraph/core/vocabulary/SD.java | 67 +++--- .../util/jena/PrefixGraphRepositoryTest.java | 99 ++++++++ 5 files changed, 381 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java create mode 100644 src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java diff --git a/pom.xml b/pom.xml index 3fe1c79a..2ba078b1 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,12 @@ + + + org.apache.jena + jena-ontapi + 6.1.0 + org.slf4j jcl-over-slf4j diff --git a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java new file mode 100644 index 00000000..e7a83485 --- /dev/null +++ b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java @@ -0,0 +1,219 @@ +/** + * Copyright 2026 Martynas Jusevičius + * + * Licensed 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 com.atomgraph.core.util.jena; + +import com.atomgraph.core.client.GraphStoreClient; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.apache.jena.graph.Graph; +import org.apache.jena.ontapi.impl.repositories.DocumentGraphRepository; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.riot.system.streammgr.LocatorClassLoader; +import org.apache.jena.riot.system.streammgr.LocatorFile; +import org.apache.jena.riot.system.streammgr.StreamManager; +import org.apache.jena.vocabulary.LocationMappingVocab; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link org.apache.jena.ontapi.GraphRepository} that resolves graph IDs to local documents, + * supporting both exact URI mappings and longest-namespace-prefix mappings. + * + * Replaces the legacy data-access stack: the {@code DataManagerImpl} loader (RDF retrieval + + * caching), the {@code PrefixMapper} ({@code LocationMapper} subclass with prefix matching), and + * the ontology {@code ModelGetter} used for {@code owl:imports} resolution. HTTP/HTTPS locations + * are loaded via the {@link GraphStoreClient}; other locations (classpath, file) via a RIOT + * {@link StreamManager}. Loaded graphs are cached by ID in the inherited repository store. + * + * @author Martynas Jusevičius {@literal } + */ +public class PrefixGraphRepository extends DocumentGraphRepository +{ + + private static final Logger log = LoggerFactory.getLogger(PrefixGraphRepository.class); + + private final Map exactLocations = new HashMap<>(); + private final Map prefixLocations = new HashMap<>(); + private final GraphStoreClient gsc; + private final StreamManager streamManager; + + /** + * Constructs the repository with the Graph Store client used for HTTP loading. + * + * @param gsc Graph Store client + */ + public PrefixGraphRepository(GraphStoreClient gsc) + { + this.gsc = gsc; + this.streamManager = new StreamManager(); // HTTP/HTTPS handled via GraphStoreClient, so only file + classpath locators needed + streamManager.addLocator(new LocatorFile()); + streamManager.addLocator(new LocatorClassLoader(getClass().getClassLoader())); + } + + /** + * Maps an exact URI to a document location. + * + * @param uri graph URI + * @param location document location (URL or classpath path) + * @return this repository + */ + public PrefixGraphRepository addLocationMapping(String uri, String location) + { + exactLocations.put(uri, location); + return this; + } + + /** + * Maps a URI namespace prefix to a document location. + * + * @param prefix URI prefix + * @param location document location (URL or classpath path) + * @return this repository + */ + public PrefixGraphRepository addPrefixMapping(String prefix, String location) + { + prefixLocations.put(prefix, location); + return this; + } + + /** + * Loads exact ({@code lm:name}/{@code lm:altName}) and prefix ({@code lm:prefix}/{@code lm:altName}) + * mappings from a Jena location-mapping configuration model. + * + * @param config location-mapping model + * @return this repository + */ + public PrefixGraphRepository processConfig(Model config) + { + StmtIterator mappings = config.listStatements(null, LocationMappingVocab.mapping, (RDFNode)null); + while (mappings.hasNext()) + { + Statement stmt = mappings.nextStatement(); + Resource mapping = stmt.getResource(); + + if (mapping.hasProperty(LocationMappingVocab.name) && mapping.hasProperty(LocationMappingVocab.altName)) + addLocationMapping(mapping.getRequiredProperty(LocationMappingVocab.name).getString(), mapping.getRequiredProperty(LocationMappingVocab.altName).getString()); + + if (mapping.hasProperty(LocationMappingVocab.prefix) && mapping.hasProperty(LocationMappingVocab.altName)) + addPrefixMapping(mapping.getRequiredProperty(LocationMappingVocab.prefix).getString(), mapping.getRequiredProperty(LocationMappingVocab.altName).getString()); + } + + return this; + } + + /** + * Resolves a graph ID to a document location: an exact mapping wins, otherwise the longest + * matching namespace prefix; if neither matches, the ID is its own location. + * + * @param id graph ID + * @return document location + */ + public String resolve(String id) + { + if (exactLocations.containsKey(id)) return exactLocations.get(id); + + String prefix = null; + for (Iterator it = prefixLocations.keySet().iterator(); it.hasNext();) + { + String candidate = it.next(); + if (id.startsWith(candidate) && (prefix == null || candidate.length() > prefix.length())) prefix = candidate; + } + if (prefix != null) return prefixLocations.get(prefix); + + return id; + } + + /** + * Returns true if the ID has a (non-HTTP) document mapping — e.g. a bundled local ontology. + * + * @param id graph ID + * @return true if mapped to a non-HTTP location + */ + public boolean isMapped(String id) + { + String location = resolve(id); + return !location.equals(id) && !location.startsWith("http://") && !location.startsWith("https://"); + } + + /** + * Returns true if a graph for the ID is already loaded into the store. + * + * @param id graph ID + * @return true if cached + */ + public boolean isCached(String id) + { + return getIds().contains(id); + } + + @Override + public Graph get(String id) + { + if (getIds().contains(id)) return super.get(id); // already loaded into the store + + String location = resolve(id); + if (log.isDebugEnabled()) log.debug("Loading graph '{}' from location '{}'", id, location); + Graph graph = load(location); + put(id, graph); + return graph; + } + + /** + * Loads a graph from a document location. HTTP/HTTPS via the Graph Store client; everything + * else (classpath, file) via the RIOT stream manager. + * + * @param location document location + * @return loaded graph + */ + protected Graph load(String location) + { + if (location.startsWith("http://") || location.startsWith("https://")) + return getGraphStoreClient().getModel(location).getGraph(); + + Model model = ModelFactory.createDefaultModel(); + RDFParser.create().source(location).streamManager(getStreamManager()).build().parse(model); + return model.getGraph(); + } + + /** + * Returns the Graph Store client used for HTTP loading. + * + * @return Graph Store client + */ + public GraphStoreClient getGraphStoreClient() + { + return gsc; + } + + /** + * Returns the stream manager used for classpath/file loading. + * + * @return stream manager + */ + public StreamManager getStreamManager() + { + return streamManager; + } + +} diff --git a/src/main/java/com/atomgraph/core/vocabulary/A.java b/src/main/java/com/atomgraph/core/vocabulary/A.java index 1b91754f..2809576c 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/A.java +++ b/src/main/java/com/atomgraph/core/vocabulary/A.java @@ -16,11 +16,11 @@ */ package com.atomgraph.core.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -29,8 +29,13 @@ */ public final class A { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "https://w3id.org/atomgraph/core#"; @@ -47,33 +52,33 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Dataset file property */ - public static final DatatypeProperty dataset = m_model.createDatatypeProperty( NS + "dataset" ); + public static final Property dataset = m_model.createDataProperty( NS + "dataset" ); /** Graph Store URL property */ - public static final ObjectProperty graphStore = m_model.createObjectProperty( NS + "graphStore" ); + public static final Property graphStore = m_model.createObjectProperty( NS + "graphStore" ); /** Quad store URL property */ - public static final ObjectProperty quadStore = m_model.createObjectProperty( NS + "quadStore" ); + public static final Property quadStore = m_model.createObjectProperty( NS + "quadStore" ); /** Cache-Control property **/ - public static final DatatypeProperty cacheControl = m_model.createDatatypeProperty( NS + "cacheControl" ); + public static final Property cacheControl = m_model.createDataProperty( NS + "cacheControl" ); /** Result limit property */ - public static final DatatypeProperty resultLimit = m_model.createDatatypeProperty( NS + "resultLimit" ); + public static final Property resultLimit = m_model.createDataProperty( NS + "resultLimit" ); /** Cache models property */ - public static final DatatypeProperty cacheModelLoads = m_model.createDatatypeProperty( NS + "cacheModelLoads" ); + public static final Property cacheModelLoads = m_model.createDataProperty( NS + "cacheModelLoads" ); /** Preemptive HTTP Basic auth property */ - public static final DatatypeProperty preemptiveAuth = m_model.createDatatypeProperty( NS + "preemptiveAuth" ); + public static final Property preemptiveAuth = m_model.createDataProperty( NS + "preemptiveAuth" ); /** Max GET request size property */ - public static final DatatypeProperty maxGetRequestSize = m_model.createDatatypeProperty( NS + "maxGetRequestSize" ); + public static final Property maxGetRequestSize = m_model.createDataProperty( NS + "maxGetRequestSize" ); /** HTTP Basic auth user property */ - public static final DatatypeProperty authUser = m_model.createDatatypeProperty( NS + "authUser" ); + public static final Property authUser = m_model.createDataProperty( NS + "authUser" ); /** HTTP Basic auth password property */ - public static final DatatypeProperty authPwd = m_model.createDatatypeProperty( NS + "authPwd" ); + public static final Property authPwd = m_model.createDataProperty( NS + "authPwd" ); } \ No newline at end of file diff --git a/src/main/java/com/atomgraph/core/vocabulary/SD.java b/src/main/java/com/atomgraph/core/vocabulary/SD.java index 2b2d7736..55111ada 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/SD.java +++ b/src/main/java/com/atomgraph/core/vocabulary/SD.java @@ -16,12 +16,11 @@ */ package com.atomgraph.core.vocabulary; -import org.apache.jena.ontology.Individual; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntClass; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; /** @@ -30,12 +29,18 @@ */ public class SD { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); - + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + /**

The namespace of the vocabulary as a string

*/ public static final String NS = "http://www.w3.org/ns/sparql-service-description#"; - + /**

The namespace of the vocabulary as a string

* @return namespace URI * @see #NS */ @@ -43,36 +48,36 @@ public static String getURI() { return NS; } - + /**

The namespace of the vocabulary as a resource

*/ public static final Resource NAMESPACE = m_model.createResource( NS ); - - public static final OntClass Dataset = m_model.createClass( NS + "Dataset" ); - public static final OntClass Service = m_model.createClass( NS + "Service" ); + public static final OntClass Dataset = m_model.createOntClass( NS + "Dataset" ); + + public static final OntClass Service = m_model.createOntClass( NS + "Service" ); + + public static final OntClass Graph = m_model.createOntClass( NS + "Graph" ); + + public static final OntClass NamedGraph = m_model.createOntClass( NS + "NamedGraph" ); + + public static final OntClass Language = m_model.createOntClass( NS + "Language" ); + + public static final Property endpoint = m_model.createObjectProperty( NS + "endpoint" ); + + public static final Property graph = m_model.createObjectProperty( NS + "graph" ); + + public static final Property name = m_model.createObjectProperty( NS + "name" ); - public static final OntClass Graph = m_model.createClass( NS + "Graph" ); - - public static final OntClass NamedGraph = m_model.createClass( NS + "NamedGraph" ); - - public static final OntClass Language = m_model.createClass( NS + "Language" ); - - public static final ObjectProperty endpoint = m_model.createObjectProperty( NS + "endpoint" ); + public static final Property defaultGraph = m_model.createObjectProperty( NS + "defaultGraph" ); - public static final ObjectProperty graph = m_model.createObjectProperty( NS + "graph" ); + public static final Property namedGraph = m_model.createObjectProperty( NS + "namedGraph" ); - public static final ObjectProperty name = m_model.createObjectProperty( NS + "name" ); + public static final Property supportedLanguage = m_model.createObjectProperty( NS + "supportedLanguage" ); - public static final ObjectProperty defaultGraph = m_model.createObjectProperty( NS + "defaultGraph" ); + public static final Resource SPARQL10Query = m_model.createIndividual(NS + "SPARQL10Query", Language); - public static final ObjectProperty namedGraph = m_model.createObjectProperty( NS + "namedGraph" ); - - public static final ObjectProperty supportedLanguage = m_model.createObjectProperty( NS + "supportedLanguage" ); - - public static final Individual SPARQL10Query = m_model.createIndividual(NS + "SPARQL10Query", Language); - - public static final Individual SPARQL11Query = m_model.createIndividual(NS + "SPARQL11Query", Language); + public static final Resource SPARQL11Query = m_model.createIndividual(NS + "SPARQL11Query", Language); - public static final Individual SPARQL11Update = m_model.createIndividual(NS + "SPARQL11Update", Language); + public static final Resource SPARQL11Update = m_model.createIndividual(NS + "SPARQL11Update", Language); } diff --git a/src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java b/src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java new file mode 100644 index 00000000..399f9b06 --- /dev/null +++ b/src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java @@ -0,0 +1,99 @@ +/** + * Copyright 2026 Martynas Jusevičius + * + * Licensed 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 com.atomgraph.core.util.jena; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.LocationMappingVocab; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Pins the URI→location resolution of {@link PrefixGraphRepository}, the replacement for the + * legacy {@code PrefixMapper}. Carries over the same longest-namespace-prefix semantics so the + * migration can be shown to retain behavior. + * + * @author Martynas Jusevičius {@literal } + */ +public class PrefixGraphRepositoryTest +{ + + private PrefixGraphRepository repo; + + @BeforeEach + public void setUp() + { + repo = new PrefixGraphRepository(null); // GraphStoreClient only needed for HTTP loads, not resolution + } + + /** The longest registered prefix that the URI starts with wins. */ + @Test + public void testResolveReturnsLongestPrefixMatch() + { + repo.addPrefixMapping("http://example.org/", "file:short.ttl"); + repo.addPrefixMapping("http://example.org/ns/", "file:long.ttl"); + + assertEquals("file:long.ttl", repo.resolve("http://example.org/ns/Foo")); + assertEquals("file:short.ttl", repo.resolve("http://example.org/other")); + } + + /** No registered mapping → the ID is its own location. */ + @Test + public void testResolveFallsBackToIdWhenNoMapping() + { + repo.addPrefixMapping("http://example.org/ns/", "file:long.ttl"); + + assertEquals("http://other.example/thing", repo.resolve("http://other.example/thing")); + } + + /** An exact location mapping wins over a prefix that also matches. */ + @Test + public void testExactMappingWinsOverPrefix() + { + repo.addPrefixMapping("http://example.org/ns/", "file:prefix.ttl"); + repo.addLocationMapping("http://example.org/ns/Exact", "file:exact.ttl"); + + assertEquals("file:exact.ttl", repo.resolve("http://example.org/ns/Exact")); + assertEquals("file:prefix.ttl", repo.resolve("http://example.org/ns/Other")); + } + + /** processConfig() reads an lm: location-mapping model into exact + prefix mappings. */ + @Test + public void testProcessConfigLoadsExactAndPrefixMappings() + { + Model config = ModelFactory.createDefaultModel(); + Resource root = config.createResource(); + + Resource exact = config.createResource(); + config.add(root, LocationMappingVocab.mapping, exact); + config.add(exact, LocationMappingVocab.name, "http://example.org/exact#"); + config.add(exact, LocationMappingVocab.altName, "file:exact.ttl"); + + Resource prefix = config.createResource(); + config.add(root, LocationMappingVocab.mapping, prefix); + config.add(prefix, LocationMappingVocab.prefix, "http://example.org/prefix/"); + config.add(prefix, LocationMappingVocab.altName, "file:prefix.ttl"); + + repo.processConfig(config); + + assertEquals("file:exact.ttl", repo.resolve("http://example.org/exact#")); + assertEquals("file:prefix.ttl", repo.resolve("http://example.org/prefix/Term")); + } + +} From 3d96818d29a9f977679b050b204660249d7a1734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 15:04:04 +0200 Subject: [PATCH 2/9] PrefixGraphRepository: expose location/prefix mappings for seeding per-app repositories Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/util/jena/PrefixGraphRepository.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java index e7a83485..733c4658 100644 --- a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java +++ b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java @@ -144,6 +144,26 @@ public String resolve(String id) return id; } + /** + * Returns the exact URI→location mappings (live view). + * + * @return exact mappings + */ + public Map getLocationMappings() + { + return exactLocations; + } + + /** + * Returns the namespace-prefix→location mappings (live view). + * + * @return prefix mappings + */ + public Map getPrefixMappings() + { + return prefixLocations; + } + /** * Returns true if the ID has a (non-HTTP) document mapping — e.g. a bundled local ontology. * From 09441b6f834e8a4dff7028203fe94c408cf9213c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 18:46:40 +0200 Subject: [PATCH 3/9] Delete DataManager/DataManagerImpl/DataManagerFactory; remove FileManager from the stack The data-access layer is now fully off the legacy org.apache.jena.util.FileManager / ontology ModelGetter. Loading + caching is GraphRepository; GSP is GraphStoreClient. Core Application no longer creates a DataManager. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/atomgraph/core/Application.java | 12 - .../core/factory/DataManagerFactory.java | 65 ----- .../atomgraph/core/util/jena/DataManager.java | 48 ---- .../core/util/jena/DataManagerImpl.java | 246 ------------------ 4 files changed, 371 deletions(-) delete mode 100644 src/main/java/com/atomgraph/core/factory/DataManagerFactory.java delete mode 100644 src/main/java/com/atomgraph/core/util/jena/DataManager.java delete mode 100644 src/main/java/com/atomgraph/core/util/jena/DataManagerImpl.java diff --git a/src/main/java/com/atomgraph/core/Application.java b/src/main/java/com/atomgraph/core/Application.java index 7697813d..d7815b5b 100644 --- a/src/main/java/com/atomgraph/core/Application.java +++ b/src/main/java/com/atomgraph/core/Application.java @@ -33,11 +33,8 @@ import com.atomgraph.core.riot.RDFLanguages; import com.atomgraph.core.riot.lang.RDFPostReaderFactory; import com.atomgraph.core.server.Dispatcher; -import com.atomgraph.core.util.jena.DataManager; -import com.atomgraph.core.util.jena.DataManagerImpl; import com.atomgraph.core.vocabulary.A; import com.atomgraph.core.vocabulary.SD; -import java.util.HashMap; import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletConfig; import jakarta.ws.rs.client.Client; @@ -48,7 +45,6 @@ import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.riot.resultset.ResultSetLang; -import org.apache.jena.util.LocationMapper; import org.glassfish.hk2.utilities.binding.AbstractBinder; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.server.ResourceConfig; @@ -73,7 +69,6 @@ public class Application extends ResourceConfig implements com.atomgraph.core.mo private final Service service; private final MediaTypes mediaTypes; private final Client client; - private final DataManager dataManager; private final Integer maxGetRequestSize; private final boolean preemptiveAuth; @@ -143,9 +138,6 @@ public Application(final Dataset dataset, ResourceFactory.createResource(endpointURI), ResourceFactory.createResource(graphStoreURI), ResourceFactory.createResource(quadStoreURI), authUser, authPwd, maxGetRequestSize); } - - dataManager = new DataManagerImpl(LocationMapper.get(), new HashMap<>(), GraphStoreClient.create(client, mediaTypes), - cacheModelLoads, preemptiveAuth); } @PostConstruct @@ -207,10 +199,6 @@ public Integer getMaxGetRequestSize() return maxGetRequestSize; } - public DataManager getDataManager() - { - return dataManager; - } public boolean isPreemptiveAuth() { diff --git a/src/main/java/com/atomgraph/core/factory/DataManagerFactory.java b/src/main/java/com/atomgraph/core/factory/DataManagerFactory.java deleted file mode 100644 index 50910dcb..00000000 --- a/src/main/java/com/atomgraph/core/factory/DataManagerFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2014 Martynas Jusevičius - * - * Licensed 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 com.atomgraph.core.factory; - -import com.atomgraph.core.util.jena.DataManager; -import org.glassfish.hk2.api.Factory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS provider for data manager. - * Needs to be registered in the JAX-RS application. - * - * @author Martynas Jusevičius {@literal } - * @see com.atomgraph.core.util.jena.DataManager - * @see jakarta.ws.rs.core.Context - */ -public class DataManagerFactory implements Factory -{ - - private static final Logger log = LoggerFactory.getLogger(DataManagerFactory.class); - - private final DataManager dataManager; - - public DataManagerFactory(final DataManager dataManager) - { - this.dataManager = dataManager; - } - - @Override - public DataManager provide() - { - return getDataManager(); - } - - @Override - public void dispose(DataManager dataManager) - { - } - - /** - * Returns default data manager instance. - * @return data manager instance - */ - public DataManager getDataManager() - { - return dataManager; - } - -} \ No newline at end of file diff --git a/src/main/java/com/atomgraph/core/util/jena/DataManager.java b/src/main/java/com/atomgraph/core/util/jena/DataManager.java deleted file mode 100644 index c09eded4..00000000 --- a/src/main/java/com/atomgraph/core/util/jena/DataManager.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 Martynas Jusevičius . - * - * Licensed 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 com.atomgraph.core.util.jena; - -import com.atomgraph.core.MediaTypes; -import java.net.URI; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.Response; -import java.util.Map; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.ontology.models.ModelGetter; - -/** - * - * @author Martynas Jusevičius {@literal } - */ -public interface DataManager extends ModelGetter -{ - - Map getModelCache(); - -// Client getClient(); - -// MediaTypes getMediaTypes(); - - //WebTarget getEndpoint(URI endpointURI); - - Response get(String uri, jakarta.ws.rs.core.MediaType[] acceptedTypes); // TO-DO: deprecate? - - boolean usePreemptiveAuth(); - - Model loadModel(String uri); - -} diff --git a/src/main/java/com/atomgraph/core/util/jena/DataManagerImpl.java b/src/main/java/com/atomgraph/core/util/jena/DataManagerImpl.java deleted file mode 100644 index 3e4ef910..00000000 --- a/src/main/java/com/atomgraph/core/util/jena/DataManagerImpl.java +++ /dev/null @@ -1,246 +0,0 @@ -/** - * Copyright 2012 Martynas Jusevičius - * - * Licensed 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 com.atomgraph.core.util.jena; - -import org.apache.jena.rdf.model.Model; -import org.apache.jena.util.LocationMapper; -import java.net.URI; -import com.atomgraph.core.client.GraphStoreClient; -import java.util.Map; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriBuilder; -import java.util.Collections; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.ModelReader; -import org.apache.jena.util.FileManagerImpl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** -* Utility class for retrieval of RDF models from remote URLs. -* -* @author Martynas Jusevičius {@literal } -* @see org.apache.jena.util.FileManager -* @see com.atomgraph.core.client.GraphStoreClient -*/ - -public class DataManagerImpl extends FileManagerImpl implements DataManager -{ - - private static final Logger log = LoggerFactory.getLogger(DataManagerImpl.class); - - private final boolean preemptiveAuth; - private final GraphStoreClient gsc; - private boolean cacheModelLoads; - private final Map modelCache; - - /** - * Creates data manager. - * - * @param mapper location mapper - * @param modelCache model cache map - * @param gsc Graph Store client - * @param cacheModelLoads if true, cache models after loading, using locations as keys - * @param preemptiveAuth if true, preemptive HTTP authentication will be used - */ - public DataManagerImpl(LocationMapper mapper, Map modelCache, - GraphStoreClient gsc, - boolean cacheModelLoads, boolean preemptiveAuth) - { - super(mapper); - if (modelCache == null) throw new IllegalArgumentException("Model cache Map must be not null"); - this.modelCache = modelCache; - this.cacheModelLoads = cacheModelLoads; - this.preemptiveAuth = preemptiveAuth; - this.gsc = gsc; - - addLocatorFile() ; - addLocatorURL() ; - addLocatorClassLoader(getClass().getClassLoader()) ; - } - - public URI getEndpoint(URI endpointURI) - { - if (endpointURI == null) throw new IllegalArgumentException("Endpoint URI must be not null"); - - return UriBuilder.fromUri(endpointURI).fragment(null).build().normalize(); // cannot use the URI class because query string with special chars such as '+' gets decoded - } - - @Override - public Response get(String uri, jakarta.ws.rs.core.MediaType[] acceptedTypes) - { - return getGraphStoreClient().get(getEndpoint(URI.create(uri)), acceptedTypes); - } - - @Override - public Model loadModel(String uri) - { - // only handle HTTP/HTTPS URIs, leave the rest to Jena - if (!hasCachedModel(uri)) - { - String mappedURI = mapURI(uri); - if (mappedURI.startsWith("http") || mappedURI.startsWith("https")) - { - Model model = getGraphStoreClient().getModel(getEndpoint(URI.create(uri)).toString()); - - if (isCachingModels()) addCacheModel(uri, model) ; - - return model; - } - } - - return super.loadModelInternal(uri); - } - - @Override - public Model getModel(String uri) - { - return loadModel(uri); - } - - @Override - public Model getModel(String uri, ModelReader loadIfAbsent) - { - Model model = getModel(uri); - - if (model == null) return loadIfAbsent.readModel(ModelFactory.createDefaultModel(), uri); - - return model; - } - - @Override - public Model readModel(Model model, String uri) - { - String mappedURI = mapURI(uri); - if (mappedURI.startsWith("http") || mappedURI.startsWith("https")) - return model.add(getGraphStoreClient().getModel(getEndpoint(URI.create(uri)).toString())); - - return super.readModelInternal(model, uri); - } - - /** - * Returns an immutable copy of the model cache - * - * @return immutable cache map - */ - @Override - public Map getModelCache() - { - return Collections.unmodifiableMap(modelCache); - } - - /** - * Reset the model cache - */ - @Override - public void resetCache() - { - modelCache.clear() ; - } - - /** - * Change the state of model cache : does not clear the cache. - * Deprecated - use constructor argument instead. - * - * @param state true to enable caching - */ - @Override - @Deprecated - public void setModelCaching(boolean state) - { - this.cacheModelLoads = state; - } - - /** - * Return whether caching is on of off - * - * @return true if caching is enabled - */ - @Override - public boolean isCachingModels() - { - return cacheModelLoads; - } - - /** - * Read out of the cache - return null if not in the cache - * - * @param filenameOrURI the location to load model from - * @return model instance or null if it's not cached or caching is off - */ - @Override - public Model getFromCache(String filenameOrURI) - { - if (!isCachingModels()) return null; - - return modelCache.get(filenameOrURI); - } - - /** - * Check if model is cached for a given URI - * - * @param filenameOrURI model location - * @return true if cached, if it's not cached or caching is off - */ - @Override - public boolean hasCachedModel(String filenameOrURI) - { - if (!isCachingModels()) return false; - - return modelCache.containsKey(filenameOrURI) ; - } - - /** - * Add model to cache using given URI as key - * - * @param uri model URI (cache key) - * @param m the model - */ - @Override - public void addCacheModel(String uri, Model m) - { - if (isCachingModels()) modelCache.put(uri, m); - } - - /** - * Remove cache from model using given URI key - * - * @param uri cache key - */ - @Override - public void removeCacheModel(String uri) - { - if (isCachingModels()) modelCache.remove(uri) ; - } - - @Override - public boolean usePreemptiveAuth() - { - return preemptiveAuth; - } - - /** - * Returns Graph Store client. - * - * @return client instance - */ - public GraphStoreClient getGraphStoreClient() - { - return gsc; - } - -} \ No newline at end of file From 1bc0ba22136d8eae6d036aed17ba882c5a8e2e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 23:05:22 +0200 Subject: [PATCH 4/9] Use OWL1_FULL_MEM instead of OWL2 profile (legacy OWL_MEM equivalent) ontapi OWL2_* profiles (DL and Full) do not recognize bare rdfs:Class as an OntClass; legacy OntModelSpec.OWL_MEM is OWL 1 Full and does. Vocab holders SD/A use OWL1_FULL_MEM. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/atomgraph/core/vocabulary/A.java | 2 +- src/main/java/com/atomgraph/core/vocabulary/SD.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/atomgraph/core/vocabulary/A.java b/src/main/java/com/atomgraph/core/vocabulary/A.java index 2809576c..b6496c7a 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/A.java +++ b/src/main/java/com/atomgraph/core/vocabulary/A.java @@ -35,7 +35,7 @@ public final class A org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it } /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL1_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "https://w3id.org/atomgraph/core#"; diff --git a/src/main/java/com/atomgraph/core/vocabulary/SD.java b/src/main/java/com/atomgraph/core/vocabulary/SD.java index 55111ada..3ad29f6a 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/SD.java +++ b/src/main/java/com/atomgraph/core/vocabulary/SD.java @@ -36,7 +36,7 @@ public class SD } /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_DL_MEM); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL1_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "http://www.w3.org/ns/sparql-service-description#"; From 3e52df2373b8050861a60d51ec829663a9503ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 19 Jun 2026 12:01:34 +0200 Subject: [PATCH 5/9] Parse bundled graphs with the graph id as base URI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PrefixGraphRepository.load parsed classpath/file documents with the file location as base, so relative URIs in the document (e.g. acl.rdf's ontology resource) resolved against the location instead of the graph URI — losing the imported ontology's terms under its canonical URI. Use the graph id as the parser base, matching the legacy FileManager/OntDocumentManager behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/util/jena/PrefixGraphRepository.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java index 733c4658..5d0b48c3 100644 --- a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java +++ b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java @@ -194,25 +194,28 @@ public Graph get(String id) String location = resolve(id); if (log.isDebugEnabled()) log.debug("Loading graph '{}' from location '{}'", id, location); - Graph graph = load(location); + Graph graph = load(id, location); put(id, graph); return graph; } /** * Loads a graph from a document location. HTTP/HTTPS via the Graph Store client; everything - * else (classpath, file) via the RIOT stream manager. + * else (classpath, file) via the RIOT stream manager. The graph id is used as the parser base so that + * relative URIs in the document (e.g. {@code rdf:about=""} for the ontology resource) resolve against the + * graph URI rather than the (classpath/file) location. * + * @param base parser base URI (the graph id) * @param location document location * @return loaded graph */ - protected Graph load(String location) + protected Graph load(String base, String location) { if (location.startsWith("http://") || location.startsWith("https://")) return getGraphStoreClient().getModel(location).getGraph(); Model model = ModelFactory.createDefaultModel(); - RDFParser.create().source(location).streamManager(getStreamManager()).build().parse(model); + RDFParser.create().source(location).base(base).streamManager(getStreamManager()).build().parse(model); return model.getGraph(); } From 36ae4f0dd4aba7d893b742bd7a1ddc6dd80aa440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 19 Jun 2026 13:58:16 +0200 Subject: [PATCH 6/9] Create SD Language individuals as typed resources (OWL1-safe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SD. ran m_model.createIndividual(...) for SPARQL10Query/SPARQL11Query/SPARQL11Update, but the OWL1 profile (OWL1_FULL_MEM, used so rdfs:Class is recognized as an OntClass) rejects named individuals — ontapi throws OntJenaException$Creation, which propagated out of Application.init() via ServiceImplementation.canWrap and prevented the webapp from starting. Create them as plain typed resources instead (identical RDF, works in any profile). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/atomgraph/core/vocabulary/SD.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/atomgraph/core/vocabulary/SD.java b/src/main/java/com/atomgraph/core/vocabulary/SD.java index 3ad29f6a..4f2fcdc3 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/SD.java +++ b/src/main/java/com/atomgraph/core/vocabulary/SD.java @@ -22,6 +22,7 @@ import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; /** * @@ -74,10 +75,12 @@ public static String getURI() public static final Property supportedLanguage = m_model.createObjectProperty( NS + "supportedLanguage" ); - public static final Resource SPARQL10Query = m_model.createIndividual(NS + "SPARQL10Query", Language); + // created as plain typed resources rather than via createIndividual(): the OWL1 profile (OWL1_FULL_MEM, needed + // for rdfs:Class recognition) rejects named individuals — ontapi throws OntJenaException$Creation otherwise + public static final Resource SPARQL10Query = m_model.createResource(NS + "SPARQL10Query").addProperty(RDF.type, Language); - public static final Resource SPARQL11Query = m_model.createIndividual(NS + "SPARQL11Query", Language); + public static final Resource SPARQL11Query = m_model.createResource(NS + "SPARQL11Query").addProperty(RDF.type, Language); - public static final Resource SPARQL11Update = m_model.createIndividual(NS + "SPARQL11Update", Language); + public static final Resource SPARQL11Update = m_model.createResource(NS + "SPARQL11Update").addProperty(RDF.type, Language); } From 0d471d963742a0eac2499e7b4c6edad1408d1253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 19 Jun 2026 15:07:14 +0200 Subject: [PATCH 7/9] Use OWL2_FULL_MEM profile in vocabularies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ontologies are normalized to owl:Class (see LinkedDataHub), so the OWL2 profile recognizes them as OntClass while also allowing named individuals — SD's Language individuals go back to createIndividual(). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/atomgraph/core/vocabulary/A.java | 2 +- src/main/java/com/atomgraph/core/vocabulary/SD.java | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/atomgraph/core/vocabulary/A.java b/src/main/java/com/atomgraph/core/vocabulary/A.java index b6496c7a..701da46e 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/A.java +++ b/src/main/java/com/atomgraph/core/vocabulary/A.java @@ -35,7 +35,7 @@ public final class A org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it } /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL1_FULL_MEM); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "https://w3id.org/atomgraph/core#"; diff --git a/src/main/java/com/atomgraph/core/vocabulary/SD.java b/src/main/java/com/atomgraph/core/vocabulary/SD.java index 4f2fcdc3..886312c4 100644 --- a/src/main/java/com/atomgraph/core/vocabulary/SD.java +++ b/src/main/java/com/atomgraph/core/vocabulary/SD.java @@ -22,7 +22,6 @@ import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.RDF; /** * @@ -37,7 +36,7 @@ public class SD } /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL1_FULL_MEM); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "http://www.w3.org/ns/sparql-service-description#"; @@ -75,12 +74,10 @@ public static String getURI() public static final Property supportedLanguage = m_model.createObjectProperty( NS + "supportedLanguage" ); - // created as plain typed resources rather than via createIndividual(): the OWL1 profile (OWL1_FULL_MEM, needed - // for rdfs:Class recognition) rejects named individuals — ontapi throws OntJenaException$Creation otherwise - public static final Resource SPARQL10Query = m_model.createResource(NS + "SPARQL10Query").addProperty(RDF.type, Language); + public static final Resource SPARQL10Query = m_model.createIndividual(NS + "SPARQL10Query", Language); - public static final Resource SPARQL11Query = m_model.createResource(NS + "SPARQL11Query").addProperty(RDF.type, Language); + public static final Resource SPARQL11Query = m_model.createIndividual(NS + "SPARQL11Query", Language); - public static final Resource SPARQL11Update = m_model.createResource(NS + "SPARQL11Update").addProperty(RDF.type, Language); + public static final Resource SPARQL11Update = m_model.createIndividual(NS + "SPARQL11Update", Language); } From f9aad3c2822190bee00fa06c1b6f29509e4dede9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sat, 20 Jun 2026 23:42:06 +0200 Subject: [PATCH 8/9] Move PrefixGraphRepository to Web-Client; replace deprecated Model.write with RDFWriter - Delete PrefixGraphRepository + test: Core is not ontology-aware, and this GraphRepository (owl:imports resolution) is used only by downstream Web-Client/LinkedDataHub - ModelProvider/DatasetProvider: use RDFWriter instead of deprecated Model.write, keeping basic (plain) RDF/XML output Co-Authored-By: Claude Opus 4.8 (1M context) --- .../atomgraph/core/io/DatasetProvider.java | 11 +- .../com/atomgraph/core/io/ModelProvider.java | 9 +- .../core/util/jena/PrefixGraphRepository.java | 242 ------------------ .../util/jena/PrefixGraphRepositoryTest.java | 99 ------- 4 files changed, 18 insertions(+), 343 deletions(-) delete mode 100644 src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java delete mode 100644 src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java diff --git a/src/main/java/com/atomgraph/core/io/DatasetProvider.java b/src/main/java/com/atomgraph/core/io/DatasetProvider.java index caa0a9e8..77a05012 100644 --- a/src/main/java/com/atomgraph/core/io/DatasetProvider.java +++ b/src/main/java/com/atomgraph/core/io/DatasetProvider.java @@ -32,8 +32,10 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFLanguages; import org.apache.jena.riot.RDFParserRegistry; +import org.apache.jena.riot.RDFWriter; import org.apache.jena.riot.RDFWriterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -112,7 +114,14 @@ public void writeTo(Dataset dataset, Class type, Type genericType, Annotation Lang lang = RDFLanguages.contentTypeToLang(formatType.toString()); // cannot be null - isWritable() checks that // if we need to provide triples, then we write only the default graph of the dataset - if (RDFLanguages.isTriples(lang)) dataset.getDefaultModel().write(entityStream, lang.getName()); + if (RDFLanguages.isTriples(lang)) + { + RDFFormat format = Lang.RDFXML.equals(lang) ? RDFFormat.RDFXML_PLAIN : RDFWriterRegistry.defaultSerialization(lang); // keep basic (plain) RDF/XML, not the abbreviated default + RDFWriter.create(). + format(format). + source(dataset.getDefaultModel()). + output(entityStream); + } else RDFDataMgr.write(entityStream, dataset, lang); } diff --git a/src/main/java/com/atomgraph/core/io/ModelProvider.java b/src/main/java/com/atomgraph/core/io/ModelProvider.java index 53c9079c..6306fe94 100644 --- a/src/main/java/com/atomgraph/core/io/ModelProvider.java +++ b/src/main/java/com/atomgraph/core/io/ModelProvider.java @@ -31,9 +31,11 @@ import jakarta.ws.rs.ext.MessageBodyWriter; import jakarta.ws.rs.ext.Provider; import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFLanguages; import org.apache.jena.riot.RDFParser; import org.apache.jena.riot.RDFParserRegistry; +import org.apache.jena.riot.RDFWriter; import org.apache.jena.riot.RDFWriterRegistry; import org.apache.jena.riot.system.ErrorHandler; import org.apache.jena.riot.system.ErrorHandlerFactory; @@ -158,7 +160,12 @@ public Model write(Model model, OutputStream os, Lang lang, String baseURI) String syntax = lang.getName(); if (log.isDebugEnabled()) log.debug("Syntax used to write Model: {}", syntax); - return model.write(os, syntax); + RDFFormat format = Lang.RDFXML.equals(lang) ? RDFFormat.RDFXML_PLAIN : RDFWriterRegistry.defaultSerialization(lang); // keep basic (plain) RDF/XML, not the abbreviated default + RDFWriter.create(). + format(format). + source(model). + output(os); + return model; } public UriInfo getUriInfo() diff --git a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java deleted file mode 100644 index 5d0b48c3..00000000 --- a/src/main/java/com/atomgraph/core/util/jena/PrefixGraphRepository.java +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright 2026 Martynas Jusevičius - * - * Licensed 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 com.atomgraph.core.util.jena; - -import com.atomgraph.core.client.GraphStoreClient; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import org.apache.jena.graph.Graph; -import org.apache.jena.ontapi.impl.repositories.DocumentGraphRepository; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.RDFNode; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.Statement; -import org.apache.jena.rdf.model.StmtIterator; -import org.apache.jena.riot.RDFParser; -import org.apache.jena.riot.system.streammgr.LocatorClassLoader; -import org.apache.jena.riot.system.streammgr.LocatorFile; -import org.apache.jena.riot.system.streammgr.StreamManager; -import org.apache.jena.vocabulary.LocationMappingVocab; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A {@link org.apache.jena.ontapi.GraphRepository} that resolves graph IDs to local documents, - * supporting both exact URI mappings and longest-namespace-prefix mappings. - * - * Replaces the legacy data-access stack: the {@code DataManagerImpl} loader (RDF retrieval + - * caching), the {@code PrefixMapper} ({@code LocationMapper} subclass with prefix matching), and - * the ontology {@code ModelGetter} used for {@code owl:imports} resolution. HTTP/HTTPS locations - * are loaded via the {@link GraphStoreClient}; other locations (classpath, file) via a RIOT - * {@link StreamManager}. Loaded graphs are cached by ID in the inherited repository store. - * - * @author Martynas Jusevičius {@literal } - */ -public class PrefixGraphRepository extends DocumentGraphRepository -{ - - private static final Logger log = LoggerFactory.getLogger(PrefixGraphRepository.class); - - private final Map exactLocations = new HashMap<>(); - private final Map prefixLocations = new HashMap<>(); - private final GraphStoreClient gsc; - private final StreamManager streamManager; - - /** - * Constructs the repository with the Graph Store client used for HTTP loading. - * - * @param gsc Graph Store client - */ - public PrefixGraphRepository(GraphStoreClient gsc) - { - this.gsc = gsc; - this.streamManager = new StreamManager(); // HTTP/HTTPS handled via GraphStoreClient, so only file + classpath locators needed - streamManager.addLocator(new LocatorFile()); - streamManager.addLocator(new LocatorClassLoader(getClass().getClassLoader())); - } - - /** - * Maps an exact URI to a document location. - * - * @param uri graph URI - * @param location document location (URL or classpath path) - * @return this repository - */ - public PrefixGraphRepository addLocationMapping(String uri, String location) - { - exactLocations.put(uri, location); - return this; - } - - /** - * Maps a URI namespace prefix to a document location. - * - * @param prefix URI prefix - * @param location document location (URL or classpath path) - * @return this repository - */ - public PrefixGraphRepository addPrefixMapping(String prefix, String location) - { - prefixLocations.put(prefix, location); - return this; - } - - /** - * Loads exact ({@code lm:name}/{@code lm:altName}) and prefix ({@code lm:prefix}/{@code lm:altName}) - * mappings from a Jena location-mapping configuration model. - * - * @param config location-mapping model - * @return this repository - */ - public PrefixGraphRepository processConfig(Model config) - { - StmtIterator mappings = config.listStatements(null, LocationMappingVocab.mapping, (RDFNode)null); - while (mappings.hasNext()) - { - Statement stmt = mappings.nextStatement(); - Resource mapping = stmt.getResource(); - - if (mapping.hasProperty(LocationMappingVocab.name) && mapping.hasProperty(LocationMappingVocab.altName)) - addLocationMapping(mapping.getRequiredProperty(LocationMappingVocab.name).getString(), mapping.getRequiredProperty(LocationMappingVocab.altName).getString()); - - if (mapping.hasProperty(LocationMappingVocab.prefix) && mapping.hasProperty(LocationMappingVocab.altName)) - addPrefixMapping(mapping.getRequiredProperty(LocationMappingVocab.prefix).getString(), mapping.getRequiredProperty(LocationMappingVocab.altName).getString()); - } - - return this; - } - - /** - * Resolves a graph ID to a document location: an exact mapping wins, otherwise the longest - * matching namespace prefix; if neither matches, the ID is its own location. - * - * @param id graph ID - * @return document location - */ - public String resolve(String id) - { - if (exactLocations.containsKey(id)) return exactLocations.get(id); - - String prefix = null; - for (Iterator it = prefixLocations.keySet().iterator(); it.hasNext();) - { - String candidate = it.next(); - if (id.startsWith(candidate) && (prefix == null || candidate.length() > prefix.length())) prefix = candidate; - } - if (prefix != null) return prefixLocations.get(prefix); - - return id; - } - - /** - * Returns the exact URI→location mappings (live view). - * - * @return exact mappings - */ - public Map getLocationMappings() - { - return exactLocations; - } - - /** - * Returns the namespace-prefix→location mappings (live view). - * - * @return prefix mappings - */ - public Map getPrefixMappings() - { - return prefixLocations; - } - - /** - * Returns true if the ID has a (non-HTTP) document mapping — e.g. a bundled local ontology. - * - * @param id graph ID - * @return true if mapped to a non-HTTP location - */ - public boolean isMapped(String id) - { - String location = resolve(id); - return !location.equals(id) && !location.startsWith("http://") && !location.startsWith("https://"); - } - - /** - * Returns true if a graph for the ID is already loaded into the store. - * - * @param id graph ID - * @return true if cached - */ - public boolean isCached(String id) - { - return getIds().contains(id); - } - - @Override - public Graph get(String id) - { - if (getIds().contains(id)) return super.get(id); // already loaded into the store - - String location = resolve(id); - if (log.isDebugEnabled()) log.debug("Loading graph '{}' from location '{}'", id, location); - Graph graph = load(id, location); - put(id, graph); - return graph; - } - - /** - * Loads a graph from a document location. HTTP/HTTPS via the Graph Store client; everything - * else (classpath, file) via the RIOT stream manager. The graph id is used as the parser base so that - * relative URIs in the document (e.g. {@code rdf:about=""} for the ontology resource) resolve against the - * graph URI rather than the (classpath/file) location. - * - * @param base parser base URI (the graph id) - * @param location document location - * @return loaded graph - */ - protected Graph load(String base, String location) - { - if (location.startsWith("http://") || location.startsWith("https://")) - return getGraphStoreClient().getModel(location).getGraph(); - - Model model = ModelFactory.createDefaultModel(); - RDFParser.create().source(location).base(base).streamManager(getStreamManager()).build().parse(model); - return model.getGraph(); - } - - /** - * Returns the Graph Store client used for HTTP loading. - * - * @return Graph Store client - */ - public GraphStoreClient getGraphStoreClient() - { - return gsc; - } - - /** - * Returns the stream manager used for classpath/file loading. - * - * @return stream manager - */ - public StreamManager getStreamManager() - { - return streamManager; - } - -} diff --git a/src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java b/src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java deleted file mode 100644 index 399f9b06..00000000 --- a/src/test/java/com/atomgraph/core/util/jena/PrefixGraphRepositoryTest.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright 2026 Martynas Jusevičius - * - * Licensed 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 com.atomgraph.core.util.jena; - -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.LocationMappingVocab; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -/** - * Pins the URI→location resolution of {@link PrefixGraphRepository}, the replacement for the - * legacy {@code PrefixMapper}. Carries over the same longest-namespace-prefix semantics so the - * migration can be shown to retain behavior. - * - * @author Martynas Jusevičius {@literal } - */ -public class PrefixGraphRepositoryTest -{ - - private PrefixGraphRepository repo; - - @BeforeEach - public void setUp() - { - repo = new PrefixGraphRepository(null); // GraphStoreClient only needed for HTTP loads, not resolution - } - - /** The longest registered prefix that the URI starts with wins. */ - @Test - public void testResolveReturnsLongestPrefixMatch() - { - repo.addPrefixMapping("http://example.org/", "file:short.ttl"); - repo.addPrefixMapping("http://example.org/ns/", "file:long.ttl"); - - assertEquals("file:long.ttl", repo.resolve("http://example.org/ns/Foo")); - assertEquals("file:short.ttl", repo.resolve("http://example.org/other")); - } - - /** No registered mapping → the ID is its own location. */ - @Test - public void testResolveFallsBackToIdWhenNoMapping() - { - repo.addPrefixMapping("http://example.org/ns/", "file:long.ttl"); - - assertEquals("http://other.example/thing", repo.resolve("http://other.example/thing")); - } - - /** An exact location mapping wins over a prefix that also matches. */ - @Test - public void testExactMappingWinsOverPrefix() - { - repo.addPrefixMapping("http://example.org/ns/", "file:prefix.ttl"); - repo.addLocationMapping("http://example.org/ns/Exact", "file:exact.ttl"); - - assertEquals("file:exact.ttl", repo.resolve("http://example.org/ns/Exact")); - assertEquals("file:prefix.ttl", repo.resolve("http://example.org/ns/Other")); - } - - /** processConfig() reads an lm: location-mapping model into exact + prefix mappings. */ - @Test - public void testProcessConfigLoadsExactAndPrefixMappings() - { - Model config = ModelFactory.createDefaultModel(); - Resource root = config.createResource(); - - Resource exact = config.createResource(); - config.add(root, LocationMappingVocab.mapping, exact); - config.add(exact, LocationMappingVocab.name, "http://example.org/exact#"); - config.add(exact, LocationMappingVocab.altName, "file:exact.ttl"); - - Resource prefix = config.createResource(); - config.add(root, LocationMappingVocab.mapping, prefix); - config.add(prefix, LocationMappingVocab.prefix, "http://example.org/prefix/"); - config.add(prefix, LocationMappingVocab.altName, "file:prefix.ttl"); - - repo.processConfig(config); - - assertEquals("file:exact.ttl", repo.resolve("http://example.org/exact#")); - assertEquals("file:prefix.ttl", repo.resolve("http://example.org/prefix/Term")); - } - -} From a19606744d17131f628a9301f5d9dfc611e76cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 21 Jun 2026 10:56:08 +0200 Subject: [PATCH 9/9] SNAPSHOT bump --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2ba078b1..9ee33556 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph core - 4.1.1-SNAPSHOT + 4.2.0-SNAPSHOT ${packaging.type} AtomGraph Core