Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## [Unreleased]
### Changed
- Application ontologies resolved as a native ontapi `owl:imports` union graph (cached per ontology URI) instead of a manually flattened, RDFS-materialized model — no RDFS inference
- `Namespace` no-query GET serves the raw ontology graph from the shared repository instead of rebuilding a repository per request

### Fixed
- Raw ontology graphs no longer leak inferred `rdf:type rdfs:Resource`, which produced multi-token `@typeof` that broke View block rendering

### Removed
- The Linked Data proxy no longer serves ontology terms; it is now dumb transport (bundled-vocab file cache + SSRF-checked external fetch), with ontology terms served by `/ns`

## [5.7.1] - 2026-08-06
### Changed
- RDFa editor: annotation overlay rebuilt on demand (`rdfa-editor/overlay.xsl`)
Expand Down
65 changes: 65 additions & 0 deletions http-tests/proxy/GET-proxied-mapped-vocab.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail

initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
purge_cache "$END_USER_VARNISH_SERVICE"
purge_cache "$ADMIN_VARNISH_SERVICE"
purge_cache "$FRONTEND_VARNISH_SERVICE"

# add agent to the readers group to be able to read documents

add-agent-to-group.sh \
-f "$OWNER_CERT_FILE" \
-p "$OWNER_CERT_PWD" \
--agent "$AGENT_URI" \
"${ADMIN_BASE_URL}acl/groups/readers/"

# well-known vocab terms that are statically prefix-mapped to bundled documents
# (src/main/resources/prefix-mapping.ttl), so the proxy serves them straight from
# that cache (isMapped branch) instead of dereferencing the network.
#
# The whole vocabulary graph is returned (tens of KiB), so assertions read from a
# here-string rather than `echo "$response" | grep -q`: `grep -q` closes the pipe on
# first match, and with `set -o pipefail` the SIGPIPE'd `echo` (write error: broken
# pipe) fails the whole pipeline whenever the response exceeds the ~64 KiB pipe buffer.
# Labels are language-tagged in the bundled documents, so the expected literal is
# matched in full including its tag.

# dct:title - slash-based namespace (http://purl.org/dc/terms/); the proxy request
# URI equals the term URI itself

dct_response=$(curl -k -f -s \
-G \
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
-H "Accept: application/n-triples" \
--data-urlencode "uri=http://purl.org/dc/terms/title" \
"$END_USER_BASE_URL")

grep -qF '<http://purl.org/dc/terms/title> <http://www.w3.org/2000/01/rdf-schema#label> "Title"@en-US' <<< "$dct_response"

# foaf:Person - also slash-based (http://xmlns.com/foaf/0.1/); label is a plain literal

foaf_response=$(curl -k -f -s \
-G \
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
-H "Accept: application/n-triples" \
--data-urlencode "uri=http://xmlns.com/foaf/0.1/Person" \
"$END_USER_BASE_URL")

grep -qF '<http://xmlns.com/foaf/0.1/Person> <http://www.w3.org/2000/01/rdf-schema#label> "Person"' <<< "$foaf_response"

# skos:Concept - hash-based namespace (http://www.w3.org/2004/02/skos/core#); the
# request carries a #fragment that ProxyRequestFilter strips before matching the
# mapped prefix, and the bundled document declares terms as relative (#Concept)
# under its own xml:base, so this also confirms that base resolves back to the
# full hash URI rather than leaking a bare fragment or the classpath location

skos_response=$(curl -k -f -s \
-G \
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
-H "Accept: application/n-triples" \
--data-urlencode "uri=http://www.w3.org/2004/02/skos/core#Concept" \
"$END_USER_BASE_URL")

grep -qF '<http://www.w3.org/2004/02/skos/core#Concept> <http://www.w3.org/2000/01/rdf-schema#label> "Concept"@en' <<< "$skos_response"
66 changes: 0 additions & 66 deletions http-tests/proxy/GET-proxied-ontology-ns.sh

This file was deleted.

42 changes: 42 additions & 0 deletions http-tests/sparql-protocol/query/GET-ns-no-query.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail

initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
purge_cache "$END_USER_VARNISH_SERVICE"
purge_cache "$ADMIN_VARNISH_SERVICE"
purge_cache "$FRONTEND_VARNISH_SERVICE"

# add a class to the app's namespace ontology

namespace_doc="${END_USER_BASE_URL}ns"
namespace="${namespace_doc}#"
ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/"
class="${namespace}ClassThree"

add-class.sh \
-f "$OWNER_CERT_FILE" \
-p "$OWNER_CERT_PWD" \
-b "$ADMIN_BASE_URL" \
--uri "$class" \
--label "Class Three" \
"$ontology_doc"

# clear ontology from memory so the new class is loaded on next request

clear-ontology.sh \
-f "$OWNER_CERT_FILE" \
-p "$OWNER_CERT_PWD" \
-b "$ADMIN_BASE_URL" \
--ontology "$namespace"

# GET <ns> with no ?query= should return the raw namespace ontology graph (asserted
# triples only, no RDFS materialization) rather than run a SPARQL query

response=$(curl -k -f -s \
-E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \
-H "Accept: application/n-triples" \
"$namespace_doc")

echo "$response" | grep -q "$class"
! echo "$response" | grep -q "http://www.w3.org/2000/01/rdf-schema#Resource"
20 changes: 18 additions & 2 deletions src/main/java/com/atomgraph/linkeddatahub/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
import javax.net.ssl.TrustManagerFactory;
import jakarta.servlet.ServletContext;
import javax.xml.transform.Source;
import org.apache.jena.ontapi.UnionGraph;
import org.apache.jena.ontapi.model.OntModel;
import org.apache.jena.query.Dataset;
import org.apache.jena.query.Query;
Expand Down Expand Up @@ -199,6 +200,7 @@
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamSource;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import net.sf.saxon.om.TreeInfo;
import net.sf.saxon.s9api.Processor;
Expand Down Expand Up @@ -288,6 +290,7 @@ public class Application extends ResourceConfig
private final KeyStore keyStore, trustStore;
private final URI secretaryWebIDURI;
private final List<Locale> supportedLanguages;
private final ExpiringMap<String, UnionGraph> ontologyGraphs = ExpiringMap.builder().maxSize(1000).expirationPolicy(ExpirationPolicy.ACCESSED).expiration(1, TimeUnit.HOURS).build(); // assembled ontology imports-closure union graphs, keyed by ontology URI; evicted entries are transparently rebuilt by OntologyFilter on the next cache miss
private final ExpiringMap<URI, Model> webIDmodelCache = ExpiringMap.builder().expiration(Long.parseLong(System.getProperty("com.atomgraph.linkeddatahub.webIDCacheExpiration", "86400")), TimeUnit.SECONDS).build(); // TTL (seconds) configurable via WEBID_CACHE_EXPIRATION; a lower value bounds how long a revoked WebID stays cached
private final ExpiringMap<String, Model> oidcModelCache = ExpiringMap.builder().variableExpiration().build();
private final ExpiringMap<String, jakarta.json.JsonObject> jwksCache = ExpiringMap.builder().expiration(Long.parseLong(System.getProperty("com.atomgraph.linkeddatahub.jwksCacheExpiration", "86400")), TimeUnit.SECONDS).build(); // Cache JWKS responses; TTL (seconds) configurable via JWKS_CACHE_EXPIRATION
Expand Down Expand Up @@ -1804,10 +1807,23 @@ public OntologyRepository createRepository(EndUserApplication app)

return appRepository;
}


/**
* Returns the cache of assembled ontology imports-closure union graphs, keyed by ontology URI
* (origin-scoped per dataspace, so a single map cannot collide across applications).
* The union graph is ontapi's view over the raw per-document graphs cached in the (per-app or
* system) repository; it is not a document graph itself and is never served on the wire.
*
* @return ontology URI to union graph map
*/
public Map<String, UnionGraph> getOntologyGraphs()
{
return ontologyGraphs;
}

/**
* Returns a registry of readable and writeable media types.
*
*
* @return registry object
*/
public MediaTypes getMediaTypes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ public Response get(@QueryParam(QUERY) Query query,
// the application ontology MUST use a <ns> URI! This is the URI this ontology endpoint is deployed on by the Dispatcher class
String ontologyURI = getApplication().getOntology().getURI();
if (log.isDebugEnabled()) log.debug("Returning raw namespace ontology: {}", ontologyURI);
// not returning the injected in-memory ontology because it has inferences applied to it;
// a fresh, mapping-seeded repository serves the raw SPARQL-loaded ontology
OntologyRepository repository = getSystem().createRepository(getApplication().as(EndUserApplication.class));
// not returning the injected in-memory ontology because it is the full imports closure (a union view);
// the shared repository serves the standalone raw ontology graph
OntologyRepository repository = getSystem().getRepository(getApplication().as(EndUserApplication.class));
return getResponseBuilder(org.apache.jena.rdf.model.ModelFactory.createModelForGraph(repository.get(ontologyURI))).build();
}
else throw new BadRequestException("SPARQL query string not provided");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer

EndUserApplication endUserApp = getApplication().as(AdminApplication.class).getEndUserApplication(); // we're assuming the current app is admin
OntologyRepository repository = getSystem().getRepository(endUserApp);
if (repository.isCached(ontologyURI))
if (repository.isCached(ontologyURI) || getSystem().getOntologyGraphs().containsKey(ontologyURI))
{
if (log.isDebugEnabled()) log.debug("Clearing ontology with URI '{}' from memory", ontologyURI);
repository.remove(ontologyURI);
getSystem().getOntologyGraphs().remove(ontologyURI);

URI ontologyDocURI = UriBuilder.fromUri(ontologyURI).fragment(null).build(); // skip fragment from the ontology URI to get its graph URI
repository.remove(ontologyDocURI.toString()); // the raw graph is also aliased under the fragment-stripped document URI
// frontend proxy still uses URL-pattern BAN for direct document GETs (until Stage 3 brings xkey tagging to varnish-frontend).
// xkey purge covers proxied SPARQL CONSTRUCT/SELECT responses tagged by their backend (varnish-admin / varnish-end-user).
URI frontendProxy = getSystem().getFrontendProxy();
Expand All @@ -110,7 +112,7 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer
}

// !!! we need to reload the ontology model before returning a response, to make sure the next request already gets the new version !!!
OntologyFilter.loadOntology(repository, ontologyURI);
getSystem().getOntologyGraphs().put(ontologyURI, OntologyFilter.loadOntology(repository, ontologyURI));
}

if (referer != null) return Response.seeOther(referer).build();
Expand Down
Loading
Loading