From 914b99b91527024465440f474032a49357ee340e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 17:30:57 +0200 Subject: [PATCH 1/7] StylesheetResolver: let Saxon resolve local xsl:import/include again Return null for non-HTTP locations so Saxon's default resolver opens file:/jar:/classpath stylesheet imports directly, instead of routing them through the RIOT StreamManager. All stylesheet imports are relative and resolve against a file:/jar: base with no location mapping, so the StreamManager hop added nothing but debug noise. HTTP imports still go through the GraphStoreClient; the repository is still used for URI->location mapping. Drops the now-unused logger and TypedInputStream. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../client/util/StylesheetResolver.java | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/atomgraph/client/util/StylesheetResolver.java b/src/main/java/com/atomgraph/client/util/StylesheetResolver.java index 083138d6..0788a5f3 100644 --- a/src/main/java/com/atomgraph/client/util/StylesheetResolver.java +++ b/src/main/java/com/atomgraph/client/util/StylesheetResolver.java @@ -28,31 +28,28 @@ import javax.xml.transform.URIResolver; import javax.xml.transform.stream.StreamSource; import org.apache.commons.io.IOUtils; -import org.apache.jena.atlas.web.TypedInputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Resolves XSLT {@code xsl:import}/{@code xsl:include} URIs to raw stylesheet {@link Source}s - * during compilation. Mapped/classpath/file locations are opened via the repository's RIOT stream - * manager; HTTP via the {@link GraphStoreClient}. Unlike {@link RDFSourceResolver} the bytes are - * returned verbatim (stylesheets are XML, not RDF). Replaces the legacy {@code XsltResolver} - * (which extended the FileManager-based {@code DataManagerImpl}). + * during compilation. HTTP locations are fetched via the {@link GraphStoreClient}; local + * ({@code file:}/{@code jar:}/classpath) locations are handed back to Saxon by returning + * {@code null}, so its default resolver opens them (relative imports against a {@code file:}/{@code jar:} + * base need no location mapping). Unlike {@link RDFSourceResolver} the bytes are returned verbatim + * (stylesheets are XML, not RDF). Replaces the legacy {@code XsltResolver} (which extended the + * FileManager-based {@code DataManagerImpl}). * * @author Martynas Jusevičius {@literal } */ public class StylesheetResolver implements URIResolver { - private static final Logger log = LoggerFactory.getLogger(StylesheetResolver.class); - private final PrefixGraphRepository repository; private final GraphStoreClient gsc; /** * Constructs the resolver. * - * @param repository graph repository for URI→location mapping and classpath/file opening + * @param repository graph repository for URI→location mapping * @param gsc Graph Store client for HTTP retrieval */ public StylesheetResolver(PrefixGraphRepository repository, GraphStoreClient gsc) @@ -80,13 +77,7 @@ public Source resolve(String href, String base) throws TransformerException } } - TypedInputStream in = getRepository().getStreamManager().open(location); - if (in == null) - { - if (log.isWarnEnabled()) log.warn("Could not resolve stylesheet location: {}", location); - return null; - } - return new StreamSource(in, uri.toString()); + return null; // non-HTTP (file:/jar:/classpath): let Saxon's default resolver open it } catch (IOException ex) { From 964018bb06f64ee51859d955995795f7a23af6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 22:14:33 +0200 Subject: [PATCH 2/7] StylesheetResolver: fetch over HTTP with plain Client, not GraphStoreClient Restore the pre-ont-api XsltResolver design: the resolver takes a plain SSL-configured JAX-RS Client and fetches http(s) stylesheet imports with Accept: text/xsl, returning null for local (file:/jar:/classpath) so Saxon's default resolver opens them. A Graph Store Protocol client has no business fetching XSLT, and stylesheet URIs are never location-mapped, so PrefixGraphRepository is dropped too. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/atomgraph/client/Application.java | 2 +- .../client/util/StylesheetResolver.java | 64 ++++++++----------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/atomgraph/client/Application.java b/src/main/java/com/atomgraph/client/Application.java index 48368cff..c6268f15 100644 --- a/src/main/java/com/atomgraph/client/Application.java +++ b/src/main/java/com/atomgraph/client/Application.java @@ -135,7 +135,7 @@ public Application(final MediaTypes mediaTypes, final Client client, final Integ try { XsltCompiler xsltComp = xsltProc.newXsltCompiler(); - xsltComp.setURIResolver(new StylesheetResolver(resolver.getRepository(), GraphStoreClient.create(client, mediaTypes))); + xsltComp.setURIResolver(new StylesheetResolver(client)); xsltExec = xsltComp.compile(stylesheet); } catch (SaxonApiException ex) diff --git a/src/main/java/com/atomgraph/client/util/StylesheetResolver.java b/src/main/java/com/atomgraph/client/util/StylesheetResolver.java index 0788a5f3..5c90d07c 100644 --- a/src/main/java/com/atomgraph/client/util/StylesheetResolver.java +++ b/src/main/java/com/atomgraph/client/util/StylesheetResolver.java @@ -16,8 +16,8 @@ */ package com.atomgraph.client.util; -import com.atomgraph.core.client.GraphStoreClient; -import com.atomgraph.client.util.jena.PrefixGraphRepository; +import com.atomgraph.client.MediaType; +import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -31,31 +31,27 @@ /** * Resolves XSLT {@code xsl:import}/{@code xsl:include} URIs to raw stylesheet {@link Source}s - * during compilation. HTTP locations are fetched via the {@link GraphStoreClient}; local - * ({@code file:}/{@code jar:}/classpath) locations are handed back to Saxon by returning - * {@code null}, so its default resolver opens them (relative imports against a {@code file:}/{@code jar:} - * base need no location mapping). Unlike {@link RDFSourceResolver} the bytes are returned verbatim - * (stylesheets are XML, not RDF). Replaces the legacy {@code XsltResolver} (which extended the - * FileManager-based {@code DataManagerImpl}). + * during compilation. HTTP(S) locations are fetched with the SSL-configured JAX-RS {@link Client} + * (app stylesheets served over HTTPS need its trust store / client certificate), requesting + * {@code text/xsl}; local ({@code file:}/{@code jar:}/classpath) locations are handed back to Saxon + * by returning {@code null}, so its default resolver opens them. Bytes are returned verbatim + * (stylesheets are XML, not RDF). * * @author Martynas Jusevičius {@literal } */ public class StylesheetResolver implements URIResolver { - private final PrefixGraphRepository repository; - private final GraphStoreClient gsc; + private final Client client; /** * Constructs the resolver. * - * @param repository graph repository for URI→location mapping - * @param gsc Graph Store client for HTTP retrieval + * @param client SSL-configured JAX-RS client for HTTP(S) stylesheet retrieval */ - public StylesheetResolver(PrefixGraphRepository repository, GraphStoreClient gsc) + public StylesheetResolver(Client client) { - this.repository = repository; - this.gsc = gsc; + this.client = client; } @Override @@ -63,21 +59,21 @@ public Source resolve(String href, String base) throws TransformerException { URI baseURI = URI.create(base); URI uri = href.isEmpty() ? baseURI : baseURI.resolve(href); - String location = getRepository().resolve(uri.toString()); - try + if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) + return null; // non-HTTP (file:/jar:/classpath): let Saxon's default resolver open it + + try (Response cr = getClient().target(uri).request().accept(MediaType.TEXT_XSL_TYPE).get()) { - if (location.startsWith("http://") || location.startsWith("https://")) + if (!cr.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)) + throw new IOException("XSLT stylesheet could not be loaded over HTTP. Status: " + cr.getStatus() + ", URI: " + uri); + + // buffer the stylesheet stream so we can close the Response + try (InputStream is = cr.readEntity(InputStream.class)) { - try (Response cr = getGraphStoreClient().getClient().target(URI.create(location)).request().get(); - InputStream is = cr.readEntity(InputStream.class)) - { - byte[] bytes = IOUtils.toByteArray(is); // buffer so we can close the Response - return new StreamSource(new ByteArrayInputStream(bytes), uri.toString()); - } + byte[] bytes = IOUtils.toByteArray(is); + return new StreamSource(new ByteArrayInputStream(bytes), uri.toString()); } - - return null; // non-HTTP (file:/jar:/classpath): let Saxon's default resolver open it } catch (IOException ex) { @@ -86,23 +82,13 @@ public Source resolve(String href, String base) throws TransformerException } /** - * Returns the graph repository. - * - * @return repository - */ - public PrefixGraphRepository getRepository() - { - return repository; - } - - /** - * Returns the Graph Store client. + * Returns the JAX-RS client used for HTTP(S) retrieval. * * @return client */ - public GraphStoreClient getGraphStoreClient() + public Client getClient() { - return gsc; + return client; } } From c7bc7a3761fd6c30ef7a269f9231ae76a4168665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:05:31 +0200 Subject: [PATCH 3/7] Fix two ontapi-migration regressions in the repository stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConstructForClass: an unreachable ontology or owl:import now degrades to an empty construct result with a warning, as OntDocumentManager used to, instead of failing the whole transform — repository loads throw unchecked Jersey/RIOT exceptions that the previous catch (IOException) missed. PrefixGraphRepository: the inherited DocumentGraphRepository store is a plain HashMap but the repository is shared across request threads (document() resolution, ontology loading, proxy caching), so concurrent first-loads could corrupt the map. Store access is now synchronized; get() double-checks so that load I/O stays outside the lock, and ids()/loadedGraphs() return snapshots taken under it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../util/jena/PrefixGraphRepository.java | 64 +++++++++++++++++-- .../writer/function/ConstructForClass.java | 25 ++++++-- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java index 22a42db0..3301818c 100644 --- a/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java +++ b/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; import org.apache.jena.graph.Graph; import org.apache.jena.ontapi.impl.repositories.DocumentGraphRepository; import org.apache.jena.rdf.model.Model; @@ -46,6 +47,9 @@ * 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. * + * The repository is shared across request threads while the inherited store is an unsynchronized + * map, so all store access is synchronized here; loading itself happens outside the lock. + * * @author Martynas Jusevičius {@literal } */ public class PrefixGraphRepository extends DocumentGraphRepository @@ -182,7 +186,7 @@ public boolean isMapped(String id) * @param id graph ID * @return true if cached */ - public boolean isCached(String id) + public synchronized boolean isCached(String id) { return getIds().contains(id); } @@ -190,13 +194,63 @@ public boolean isCached(String id) @Override public Graph get(String id) { - if (getIds().contains(id)) return super.get(id); // already loaded into the store + synchronized (this) + { + 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; + Graph graph = load(id, location); // I/O outside the lock — concurrent first loads may duplicate work + + synchronized (this) + { + if (getIds().contains(id)) return super.get(id); // lost the race — another thread loaded it first + put(id, graph); + return graph; + } + } + + @Override + public synchronized Graph put(String id, Graph graph) + { + return super.put(id, graph); + } + + @Override + public synchronized Graph remove(String id) + { + return super.remove(id); + } + + @Override + public synchronized void clear() + { + super.clear(); + } + + @Override + public synchronized boolean contains(String id) + { + return super.contains(id); + } + + @Override + public synchronized long count() + { + return super.count(); + } + + @Override + public synchronized Stream ids() + { + return super.ids().toList().stream(); // snapshot under the lock — a live stream would read the store unsynchronized + } + + @Override + public synchronized Stream loadedGraphs() + { + return super.loadedGraphs().toList().stream(); // snapshot under the lock — a live stream would read the store unsynchronized } /** diff --git a/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java b/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java index 39eaa855..c7168da2 100644 --- a/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java +++ b/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java @@ -39,6 +39,8 @@ import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * ac:construct() XSLT function that constructs instances for given classes from their constructors. @@ -49,7 +51,9 @@ */ public class ConstructForClass implements ExtensionFunction { - + + private static final Logger log = LoggerFactory.getLogger(ConstructForClass.class); + private final Processor processor; private final PrefixGraphRepository repository; @@ -91,13 +95,20 @@ public XdmValue call(XdmValue[] arguments) throws SaxonApiException String base = arguments[2].itemAt(0).getStringValue(); Model instances = ModelFactory.createDefaultModel(); - OntModel ontModel = OntModelFactory.createModel(getRepository().get(ontology), OntSpecification.OWL2_FULL_MEM, getRepository()); + try + { + OntModel ontModel = OntModelFactory.createModel(getRepository().get(ontology), OntSpecification.OWL2_FULL_MEM, getRepository()); + + arguments[1].stream(). + map(forClass -> ontModel.getOntClass(checkURI(forClass.getStringValue()).toString())). + filter(forClass -> forClass != null). + forEach(forClass -> new Constructor().construct(forClass, instances, base)); + } + catch (RuntimeException ex) // ontology or one of its imports could not be loaded — return empty result instead of failing the transform + { + if (log.isWarnEnabled()) log.warn("Could not construct instances for ontology '{}': {}", ontology, ex.toString()); + } - arguments[1].stream(). - map(forClass -> ontModel.getOntClass(checkURI(forClass.getStringValue()).toString())). - filter(forClass -> forClass != null). - forEach(forClass -> new Constructor().construct(forClass, instances, base)); - return getProcessor().newDocumentBuilder().build(getSource(instances)); } catch (IOException ex) From 5802c53e1425078dbd6e5bfe5de34b375a3a20bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:38:27 +0200 Subject: [PATCH 4/7] PrefixGraphRepository: bound the cache for mapped documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The store keyed graphs by URI, so an actor minting distinct URIs under a mapped prefix (e.g. http://xmlns.com/foaf/0.1/) grew it without bound — each URI cached its own copy of the same bundled ontology. Bundled (non-HTTP mapped) documents are now cached by their resolved location, so every URI in a namespace shares one graph and the cache is bounded by the number of bundled files. Also require the prefix match to land on a URI boundary (/ or #) so an unrelated URI can't resolve to a mapped location. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../util/jena/PrefixGraphRepository.java | 57 ++++++++++++++++--- .../util/jena/PrefixGraphRepositoryTest.java | 35 ++++++++++++ .../atomgraph/client/test/mint-ontology.ttl | 8 +++ 3 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/com/atomgraph/client/test/mint-ontology.ttl diff --git a/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java index 3301818c..2fa0913b 100644 --- a/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java +++ b/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java @@ -50,6 +50,12 @@ * The repository is shared across request threads while the inherited store is an unsynchronized * map, so all store access is synchronized here; loading itself happens outside the lock. * + * Bundled (non-HTTP mapped) documents are cached by their resolved location rather than by graph + * ID, so that every URI in a mapped namespace shares a single graph. This bounds the cache to the + * number of bundled files regardless of how many distinct URIs are requested — otherwise an actor + * minting distinct URIs under a mapped prefix (e.g. {@code http://xmlns.com/foaf/0.1/}) would + * grow the store without bound. + * * @author Martynas Jusevičius {@literal } */ public class PrefixGraphRepository extends DocumentGraphRepository @@ -59,6 +65,7 @@ public class PrefixGraphRepository extends DocumentGraphRepository private final Map exactLocations = new HashMap<>(); private final Map prefixLocations = new HashMap<>(); + private final Map mappedGraphs = new HashMap<>(); // bundled documents cached by resolved location, shared across all URIs in the namespace private final GraphStoreClient gsc; private final StreamManager streamManager; @@ -141,13 +148,35 @@ public String resolve(String id) 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 (matchesPrefix(id, candidate) && (prefix == null || candidate.length() > prefix.length())) prefix = candidate; } if (prefix != null) return prefixLocations.get(prefix); return id; } + /** + * Returns true if the id falls under the namespace prefix at a URI boundary: the id equals the + * prefix, the prefix already ends at a delimiter, or the character following the prefix in the id + * is a {@code /} or {@code #}. Prevents an unrelated URI (e.g. {@code …/foobar}) from matching a + * shorter prefix (e.g. {@code …/foo}) and resolving to the wrong mapped location. + * + * @param id graph ID + * @param prefix candidate namespace prefix + * @return true if the id is within the prefix namespace + */ + protected static boolean matchesPrefix(String id, String prefix) + { + if (!id.startsWith(prefix)) return false; + if (id.length() == prefix.length()) return true; + + char last = prefix.charAt(prefix.length() - 1); + if (last == '/' || last == '#') return true; + + char next = id.charAt(prefix.length()); + return next == '/' || next == '#'; + } + /** * Returns the exact URI→location mappings (live view). * @@ -188,24 +217,33 @@ public boolean isMapped(String id) */ public synchronized boolean isCached(String id) { - return getIds().contains(id); + return getIds().contains(id) || mappedGraphs.containsKey(resolve(id)); } @Override public Graph get(String id) { + String location; synchronized (this) { - if (getIds().contains(id)) return super.get(id); // already loaded into the store + if (getIds().contains(id)) return super.get(id); // explicitly cached (e.g. a materialized ontology) or non-mapped + location = resolve(id); + Graph mapped = mappedGraphs.get(location); + if (mapped != null) return mapped; // bundled document already loaded — shared across the namespace } - String location = resolve(id); if (log.isDebugEnabled()) log.debug("Loading graph '{}' from location '{}'", id, location); Graph graph = load(id, location); // I/O outside the lock — concurrent first loads may duplicate work + // bundled (non-HTTP mapped) documents are cached by location so every URI in the namespace shares one + // graph, bounding the store; everything else (HTTP, identity) is cached by ID + boolean mapped = !location.equals(id) && !location.startsWith("http://") && !location.startsWith("https://"); + synchronized (this) { - if (getIds().contains(id)) return super.get(id); // lost the race — another thread loaded it first + if (mapped) return mappedGraphs.computeIfAbsent(location, k -> graph); // loser of a race discards its graph + + if (getIds().contains(id)) return super.get(id); put(id, graph); return graph; } @@ -220,25 +258,28 @@ public synchronized Graph put(String id, Graph graph) @Override public synchronized Graph remove(String id) { - return super.remove(id); + Graph removed = super.remove(id); + Graph mapped = mappedGraphs.remove(resolve(id)); + return removed != null ? removed : mapped; } @Override public synchronized void clear() { super.clear(); + mappedGraphs.clear(); } @Override public synchronized boolean contains(String id) { - return super.contains(id); + return super.contains(id) || mappedGraphs.containsKey(resolve(id)); } @Override public synchronized long count() { - return super.count(); + return super.count() + mappedGraphs.size(); } @Override diff --git a/src/test/java/com/atomgraph/client/util/jena/PrefixGraphRepositoryTest.java b/src/test/java/com/atomgraph/client/util/jena/PrefixGraphRepositoryTest.java index c389f5cd..a614f4a0 100644 --- a/src/test/java/com/atomgraph/client/util/jena/PrefixGraphRepositoryTest.java +++ b/src/test/java/com/atomgraph/client/util/jena/PrefixGraphRepositoryTest.java @@ -16,6 +16,7 @@ */ package com.atomgraph.client.util.jena; +import org.apache.jena.graph.Graph; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; @@ -96,4 +97,38 @@ public void testProcessConfigLoadsExactAndPrefixMappings() assertEquals("file:prefix.ttl", repo.resolve("http://example.org/prefix/Term")); } + /** + * A prefix only matches at a namespace boundary — the id equals the prefix, or the character + * after the prefix is a {@code /} or {@code #}. An unrelated URI that merely shares the prefix + * string (e.g. {@code …/ns-evil}) must not resolve to the mapped location. + */ + @Test + public void testResolveRespectsPrefixBoundary() + { + repo.addPrefixMapping("http://example.org/ns", "file:ns.ttl"); // no trailing delimiter + + assertEquals("file:ns.ttl", repo.resolve("http://example.org/ns"), "exact namespace root"); + assertEquals("file:ns.ttl", repo.resolve("http://example.org/ns#Term"), "hash boundary"); + assertEquals("file:ns.ttl", repo.resolve("http://example.org/ns/Term"), "slash boundary"); + assertEquals("http://example.org/nsEvil", repo.resolve("http://example.org/nsEvil"), "no boundary — must not match"); + assertEquals("http://example.org/ns-evil", repo.resolve("http://example.org/ns-evil"), "no boundary — must not match"); + } + + /** + * A malicious actor minting an unbounded number of distinct URIs under a mapped prefix must not + * grow the cache: every URI in a mapped namespace resolves to the same bundled document, so they + * share one graph instance and the store stays bounded by the number of bundled files. + */ + @Test + public void testMappedGraphCacheDoesNotGrowWithDistinctURIs() + { + repo.addPrefixMapping("http://example.org/ns/", "com/atomgraph/client/test/mint-ontology.ttl"); + + Graph first = repo.get("http://example.org/ns/term0"); + for (int i = 1; i < 1000; i++) + assertSame(first, repo.get("http://example.org/ns/term" + i), "all URIs in a mapped namespace must share one graph instance"); + + assertEquals(1, repo.count(), "1000 distinct mapped URIs must not grow the cache beyond the single bundled document"); + } + } diff --git a/src/test/resources/com/atomgraph/client/test/mint-ontology.ttl b/src/test/resources/com/atomgraph/client/test/mint-ontology.ttl new file mode 100644 index 00000000..ccd189ed --- /dev/null +++ b/src/test/resources/com/atomgraph/client/test/mint-ontology.ttl @@ -0,0 +1,8 @@ +@prefix rdfs: . +@prefix owl: . +@base . + +<> a owl:Ontology . + +<#Thing> a owl:Class ; + rdfs:label "Thing" . From 9667f4dd55188d79e3b722009a91ed366855821c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:38:27 +0200 Subject: [PATCH 5/7] Drop dead preemptiveAuth constructor param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The param was accepted by the Application constructor but never stored or used — preemptive HTTP Basic auth has no place in the post-ont-api stack (delegation filters handle authenticated dereferencing). Follows the removal of the flag from Core. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/atomgraph/client/Application.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/atomgraph/client/Application.java b/src/main/java/com/atomgraph/client/Application.java index c6268f15..4208615f 100644 --- a/src/main/java/com/atomgraph/client/Application.java +++ b/src/main/java/com/atomgraph/client/Application.java @@ -103,7 +103,6 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti { this(new MediaTypes(), getClient(new ClientConfig()), servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI())) : null, - servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI())) : false, getResolver(servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()), com.atomgraph.client.Application.getClient(new ClientConfig()), new MediaTypes(), @@ -114,7 +113,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti ); } - public Application(final MediaTypes mediaTypes, final Client client, final Integer maxGetRequestSize, final boolean preemptiveAuth, + public Application(final MediaTypes mediaTypes, final Client client, final Integer maxGetRequestSize, final RDFSourceResolver resolver, final Source stylesheet, final boolean cacheStylesheet, final boolean resolvingUncached) { this.mediaTypes = mediaTypes; From d2360ef8be7fa784b5709739a9f41fd8b3bdeb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:52:59 +0200 Subject: [PATCH 6/7] PrefixGraphRepository: make the dynamic-tier store pluggable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Back the URI-keyed (dynamically loaded) graphs with a map from a protected createStore() factory instead of the inherited DocumentGraphRepository store. The default is an unbounded HashMap, so behavior is unchanged here — but subclasses can now supply a bounded/evicting map without Web-Client taking on any eviction logic or dependency. The location-keyed bundled-document cache stays separate and unbounded (bounded by file count). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../util/jena/PrefixGraphRepository.java | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java b/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java index 2fa0913b..3106871f 100644 --- a/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java +++ b/src/main/java/com/atomgraph/client/util/jena/PrefixGraphRepository.java @@ -17,8 +17,10 @@ package com.atomgraph.client.util.jena; import com.atomgraph.core.client.GraphStoreClient; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.apache.jena.graph.Graph; @@ -45,10 +47,11 @@ * 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. + * {@link StreamManager}. Dynamically loaded graphs are cached by ID in a store obtained from + * {@link #createStore()} (overridable, e.g. to supply a bounded/evicting map). * - * The repository is shared across request threads while the inherited store is an unsynchronized - * map, so all store access is synchronized here; loading itself happens outside the lock. + * The repository is shared across request threads while the store is a plain unsynchronized map, so + * all store access is synchronized here; loading itself happens outside the lock. * * Bundled (non-HTTP mapped) documents are cached by their resolved location rather than by graph * ID, so that every URI in a mapped namespace shares a single graph. This bounds the cache to the @@ -65,10 +68,24 @@ public class PrefixGraphRepository extends DocumentGraphRepository private final Map exactLocations = new HashMap<>(); private final Map prefixLocations = new HashMap<>(); + private final Map store = createStore(); // dynamically loaded graphs, keyed by graph ID private final Map mappedGraphs = new HashMap<>(); // bundled documents cached by resolved location, shared across all URIs in the namespace private final GraphStoreClient gsc; private final StreamManager streamManager; + /** + * Creates the backing store for dynamically loaded graphs (keyed by graph ID). The default is an + * unbounded {@link HashMap}; subclasses may override to supply a bounded/evicting map. The returned + * map must not depend on subclass state — it is created during construction. The location-keyed + * bundled-document cache is separate and always unbounded (bounded by the number of bundled files). + * + * @return dynamic-graph store + */ + protected Map createStore() + { + return new HashMap<>(); + } + /** * Constructs the repository with the Graph Store client used for HTTP loading. * @@ -217,7 +234,7 @@ public boolean isMapped(String id) */ public synchronized boolean isCached(String id) { - return getIds().contains(id) || mappedGraphs.containsKey(resolve(id)); + return store.containsKey(id) || mappedGraphs.containsKey(resolve(id)); } @Override @@ -226,7 +243,8 @@ public Graph get(String id) String location; synchronized (this) { - if (getIds().contains(id)) return super.get(id); // explicitly cached (e.g. a materialized ontology) or non-mapped + Graph cached = store.get(id); // explicitly cached (e.g. a materialized ontology) or non-mapped + if (cached != null) return cached; location = resolve(id); Graph mapped = mappedGraphs.get(location); if (mapped != null) return mapped; // bundled document already loaded — shared across the namespace @@ -243,8 +261,9 @@ public Graph get(String id) { if (mapped) return mappedGraphs.computeIfAbsent(location, k -> graph); // loser of a race discards its graph - if (getIds().contains(id)) return super.get(id); - put(id, graph); + Graph raced = store.get(id); + if (raced != null) return raced; // lost the race — another thread loaded it first + store.put(id, graph); return graph; } } @@ -252,13 +271,13 @@ public Graph get(String id) @Override public synchronized Graph put(String id, Graph graph) { - return super.put(id, graph); + return store.put(id, graph); } @Override public synchronized Graph remove(String id) { - Graph removed = super.remove(id); + Graph removed = store.remove(id); Graph mapped = mappedGraphs.remove(resolve(id)); return removed != null ? removed : mapped; } @@ -266,32 +285,34 @@ public synchronized Graph remove(String id) @Override public synchronized void clear() { - super.clear(); + store.clear(); mappedGraphs.clear(); } @Override public synchronized boolean contains(String id) { - return super.contains(id) || mappedGraphs.containsKey(resolve(id)); + return store.containsKey(id) || mappedGraphs.containsKey(resolve(id)); } @Override public synchronized long count() { - return super.count() + mappedGraphs.size(); + return store.size() + mappedGraphs.size(); } @Override public synchronized Stream ids() { - return super.ids().toList().stream(); // snapshot under the lock — a live stream would read the store unsynchronized + return List.copyOf(store.keySet()).stream(); // snapshot under the lock — a live stream would read the store unsynchronized } @Override public synchronized Stream loadedGraphs() { - return super.loadedGraphs().toList().stream(); // snapshot under the lock — a live stream would read the store unsynchronized + List loaded = new ArrayList<>(store.values()); + loaded.addAll(mappedGraphs.values()); + return loaded.stream(); // snapshot under the lock — a live stream would read the store unsynchronized } /** From 523ebf23dfc5e5dd7d5152604cd5eb73fe7114c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 6 Jul 2026 00:26:09 +0200 Subject: [PATCH 7/7] Core SNAPSHOT bump --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b463a19d..6dd0c8f0 100644 --- a/pom.xml +++ b/pom.xml @@ -118,7 +118,7 @@ ${project.groupId} core - 5.0.1 + 5.0.2-SNAPSHOT