diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ed2dae2c..66d9c2c1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [Unreleased] +### Changed +- Application ontologies resolved as a native ontapi `owl:imports` union graph (cached per ontology URI), no RDFS inference — replaces the manually flattened, RDFS-materialized model +- `Namespace` no-query GET serves the raw ontology graph from the shared repository instead of rebuilding one per request +- **BREAKING**: "Add data" and "Generate containers" orchestrated client-side over the Graph Store Protocol (POST-append via `?uri=` proxy; per-class container PUT fan-out embedding the view as `ldh:Object` → `rdf:value` → `ldh:View`), replacing the `/add` and `/generate` endpoints + +### Fixed +- Raw ontology graphs no longer leak inferred `rdf:type rdfs:Resource` that broke View block rendering via multi-token `@typeof` + +### Removed +- Linked Data proxy no longer serves ontology terms (now dumb transport: bundled-vocab file cache + SSRF-checked external fetch); ontology terms served by `/ns` +- **BREAKING**: `/add` and `/generate` server-side endpoints (`Add`/`Generate` JAX-RS resources), superseded by the client-orchestrated writes; removes their server-side fetch/SSRF surface (LNK-002); `/transform` retained until a client-side SPARQL engine lands + ## [5.7.1] - 2026-08-06 ### Changed - RDFa editor: annotation overlay rebuilt on demand (`rdfa-editor/overlay.xsl`) diff --git a/http-tests/add/GET-proxied-source-POST-append.sh b/http-tests/add/GET-proxied-source-POST-append.sh new file mode 100755 index 0000000000..5db813ac2c --- /dev/null +++ b/http-tests/add/GET-proxied-source-POST-append.sh @@ -0,0 +1,54 @@ +#!/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" + +# Exercises the client-orchestrated "Add data" flow that replaced the server-side /add endpoint: +# the browser GETs the external source through the same-origin ?uri= proxy as RDF/XML, then +# POSTs (appends) it to the target document. Two requests, no /add endpoint. + +# add agent to the readers group (to read through the proxy) and the writers group (to append) + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/readers/" + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/writers/" + +# create the target container + +container=$(create-container.sh \ + -f "$AGENT_CERT_FILE" \ + -p "$AGENT_CERT_PWD" \ + -b "$END_USER_BASE_URL" \ + --title "Test" \ + --slug "test" \ + --parent "$END_USER_BASE_URL") + +# step 1: fetch the external source through the LDH proxy, converted to RDF/XML + +source_rdfxml=$(curl -k -f -s -G \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H "Accept: application/rdf+xml" \ + --data-urlencode "uri=https://orcid.org/0000-0003-1750-9906" \ + "$END_USER_BASE_URL") + +# step 2: append the fetched triples to the target container document (GSP append -> 204) + +echo "$source_rdfxml" | curl -k -w "%{http_code}\n" -o /dev/null -s \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/rdf+xml" \ + --data-binary @- \ + "$container" \ +| grep -q "$STATUS_NO_CONTENT" diff --git a/http-tests/add/POST-add.sh b/http-tests/add/POST-add.sh deleted file mode 100755 index dcca9c6d8f..0000000000 --- a/http-tests/add/POST-add.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/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" - -# add agent to the writers group - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# create container - -slug="test" - -container=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Test" \ - --slug "$slug" \ - --parent "$END_USER_BASE_URL") - -# import data into the container - -curl -w "%{http_code}\n" -o /dev/null -k -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${container}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_NO_CONTENT" diff --git a/http-tests/add/PUT-generate-container.sh b/http-tests/add/PUT-generate-container.sh new file mode 100755 index 0000000000..9500e29a1d --- /dev/null +++ b/http-tests/add/PUT-generate-container.sh @@ -0,0 +1,82 @@ +#!/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" + +# Exercises the client-orchestrated "Generate containers" flow that replaced the server-side +# /generate endpoint. The client builds one container document per checked class -- a dh:Container +# whose content block is an ldh:Object wrapping an ldh:View over a $type-parameterized SELECT -- and +# PUTs it. This test PUTs one such container (shaped exactly like ldh:generate-container-doc output) +# and verifies: creation succeeds (the Object-wrapped block passes ldh:InvalidContentBlockType / +# MissingValue / MissingQuery validation), the server stamps metadata, and the block persists. + +# add agent to the writers group + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/writers/" + +parent="$END_USER_BASE_URL" +uuid=$(uuidgen | tr '[:upper:]' '[:lower:]') +container="${parent}${uuid}/" +class="https://www.w3.org/ns/ldt/document-hierarchy#Container" + +# PUT the generated container document (blank nodes are skolemized server-side) + +http_code=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X PUT \ + -H "Content-Type: application/rdf+xml" \ + --data-binary @- \ + "$container" < + + + + Containers + ${uuid} + + + + + + + + + + Select Container + SELECT DISTINCT ?s WHERE { ?s a <${class}> ; ?p ?o } + + + + + + + + +EOF +) + +[ "$http_code" = "$STATUS_CREATED" ] + +# fetch the created container and verify the shape + server-stamped metadata + +ntriples=$(curl -k -f -s \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H "Accept: application/n-triples" \ + "$container") + +# parent link, generated title, and server-stamped creation date +echo "$ntriples" | grep -q " <${parent}>" +echo "$ntriples" | grep -q " \"Containers\"" +echo "$ntriples" | grep -q "" + +# content block persisted as an ldh:Object, and the SELECT carries the substituted class IRI +echo "$ntriples" | grep -q "" +echo "$ntriples" | grep "" | grep -q "${class}" diff --git a/http-tests/proxy/GET-proxied-mapped-vocab.sh b/http-tests/proxy/GET-proxied-mapped-vocab.sh new file mode 100755 index 0000000000..f2250e07a0 --- /dev/null +++ b/http-tests/proxy/GET-proxied-mapped-vocab.sh @@ -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 ' "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 ' "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 ' "Concept"@en' <<< "$skos_response" diff --git a/http-tests/proxy/GET-proxied-ontology-ns.sh b/http-tests/proxy/GET-proxied-ontology-ns.sh deleted file mode 100755 index c97f48505a..0000000000 --- a/http-tests/proxy/GET-proxied-ontology-ns.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/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/" - -# use a made-up hash-based namespace: not mapped as a static file, not a registered app -namespace_uri="http://made-up-test-ns.example/ns" -class1="${namespace_uri}#ClassOne" -class2="${namespace_uri}#ClassTwo" -ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" -namespace="${END_USER_BASE_URL}ns#" - -# add two classes with URIs in the made-up namespace to the app's ontology - -add-class.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - -b "$ADMIN_BASE_URL" \ - --uri "$class1" \ - --label "Class One" \ - "$ontology_doc" - -add-class.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - -b "$ADMIN_BASE_URL" \ - --uri "$class2" \ - --label "Class Two" \ - "$ontology_doc" - -# clear the in-memory ontology so the new classes are present on next request - -clear-ontology.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - -b "$ADMIN_BASE_URL" \ - --ontology "$namespace" - -# request the namespace document URI (without fragment) via ?uri= proxy. -# the namespace document is not DataManager-mapped and not a registered app, -# so ProxyRequestFilter falls through to the OntModel DESCRIBE path, which -# returns descriptions of all #-fragment terms in that namespace. - -response=$(curl -k -f -s \ - -G \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -H "Accept: application/n-triples" \ - --data-urlencode "uri=${namespace_uri}" \ - "$END_USER_BASE_URL") - -# verify both class descriptions are present in the response - -echo "$response" | grep -q "$class1" -echo "$response" | grep -q "$class2" diff --git a/http-tests/proxy/POST-proxied-cross-origin-query.sh b/http-tests/proxy/POST-proxied-cross-origin-query.sh new file mode 100644 index 0000000000..989d5591c7 --- /dev/null +++ b/http-tests/proxy/POST-proxied-cross-origin-query.sh @@ -0,0 +1,37 @@ +#!/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" + +# Execute a SPARQL query against a cross-origin endpoint (the admin app's SPARQL +# endpoint) using the end-user app as a proxy. Because admin.localhost is a different +# origin than the end-user app, the request goes through ProxyRequestFilter, which +# fetches the remote endpoint and re-serializes the SPARQL results back to the caller. +# The owner is used because the admin SPARQL endpoint is ACL-protected. + +response_body=$(curl -k -s \ + -X POST \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H 'Content-Type: application/sparql-query' \ + -H 'Accept: application/sparql-results+xml' \ + --url-query "uri=${ADMIN_BASE_URL}sparql" \ + --data 'SELECT (COUNT(*) AS ?count) WHERE { ?s ?p ?o }' \ + "$END_USER_BASE_URL") + +http_code=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -X POST \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H 'Content-Type: application/sparql-query' \ + -H 'Accept: application/sparql-results+xml' \ + --url-query "uri=${ADMIN_BASE_URL}sparql" \ + --data 'SELECT (COUNT(*) AS ?count) WHERE { ?s ?p ?o }' \ + "$END_USER_BASE_URL") + +# verify successful status and that the proxy re-serialized actual SPARQL results +if [ "$http_code" -ne 200 ] || [[ "$response_body" != *"http://www.w3.org/2005/sparql-results#"* ]]; then + exit 1 +fi diff --git a/http-tests/proxy/POST-proxied-external-query.sh b/http-tests/proxy/POST-proxied-external-query.sh deleted file mode 100644 index 18e33ba81a..0000000000 --- a/http-tests/proxy/POST-proxied-external-query.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/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 writers group - POST requests count as write operations - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# execute SPARQL query using LDH as a proxy to query DBpedia - -response_body=$(curl -k -s \ - -X POST \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -H 'Content-Type: application/sparql-query' \ - -H 'Accept: application/sparql-results+xml' \ - --url-query "uri=https://dbpedia.org/sparql" \ - --data 'SELECT ?title WHERE { ?title } LIMIT 1' \ - "$END_USER_BASE_URL") - -http_code=$(curl -k -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -H 'Content-Type: application/sparql-query' \ - -H 'Accept: application/sparql-results+xml' \ - --url-query "uri=https://dbpedia.org/sparql" \ - --data 'SELECT ?title WHERE { ?title } LIMIT 1' \ - "$END_USER_BASE_URL") - -# verify response has non-empty body and successful status -if [ "$http_code" -ne 200 ] || [ -z "$response_body" ]; then - exit 1 -fi \ No newline at end of file diff --git a/http-tests/sparql-protocol/query/GET-ns-no-query.sh b/http-tests/sparql-protocol/query/GET-ns-no-query.sh new file mode 100755 index 0000000000..c2e99f7595 --- /dev/null +++ b/http-tests/sparql-protocol/query/GET-ns-no-query.sh @@ -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 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" diff --git a/http-tests/system/end-user/POST-add-401.sh b/http-tests/system/end-user/POST-add-401.sh deleted file mode 100755 index bb927b19a5..0000000000 --- a/http-tests/system/end-user/POST-add-401.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/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" - -# POST /add without a certificate should return 401 -# Only owners and writers have acl:Append access to /add via write-append authorization - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${END_USER_BASE_URL}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_UNAUTHORIZED" diff --git a/http-tests/system/end-user/POST-add-403.sh b/http-tests/system/end-user/POST-add-403.sh deleted file mode 100755 index 68d43ea7e1..0000000000 --- a/http-tests/system/end-user/POST-add-403.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/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" - -# POST /add with a signed-up agent not in any group should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${END_USER_BASE_URL}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-add-readers-403.sh b/http-tests/system/end-user/POST-add-readers-403.sh deleted file mode 100755 index d512b3ee42..0000000000 --- a/http-tests/system/end-user/POST-add-readers-403.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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" - -# POST /add with a reader should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only; -# readers only have acl:Read on dh:Item/Container and /sparql, not on /add - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/readers/" - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${END_USER_BASE_URL}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-add.sh b/http-tests/system/end-user/POST-add.sh deleted file mode 100755 index 40f715c763..0000000000 --- a/http-tests/system/end-user/POST-add.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/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 writers group - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# create container to hold the cloned data - -slug=$(uuidgen | tr '[:upper:]' '[:lower:]') - -container=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Test container" \ - --slug "$slug" \ - --parent "$END_USER_BASE_URL") - -# POST /add with a writer should succeed -# Clone data from a remote RDF source into the container - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${container}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_NO_CONTENT" diff --git a/http-tests/system/end-user/POST-generate-401.sh b/http-tests/system/end-user/POST-generate-401.sh deleted file mode 100755 index a99df03753..0000000000 --- a/http-tests/system/end-user/POST-generate-401.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/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" - -# POST /generate without a certificate should return 401 -# Only owners and writers have acl:Append access to /generate via write-append authorization - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -[] sioc:has_parent <${END_USER_BASE_URL}> . -EOF -) \ -| grep -q "$STATUS_UNAUTHORIZED" diff --git a/http-tests/system/end-user/POST-generate-403.sh b/http-tests/system/end-user/POST-generate-403.sh deleted file mode 100755 index 70838dafb7..0000000000 --- a/http-tests/system/end-user/POST-generate-403.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/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" - -# POST /generate with a signed-up agent not in any group should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -[] sioc:has_parent <${END_USER_BASE_URL}> . -EOF -) \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-generate-readers-403.sh b/http-tests/system/end-user/POST-generate-readers-403.sh deleted file mode 100755 index 751aeb8660..0000000000 --- a/http-tests/system/end-user/POST-generate-readers-403.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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" - -# POST /generate with a reader should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only; -# readers only have acl:Read on dh:Item/Container and /sparql, not on /generate - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/readers/" - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -[] sioc:has_parent <${END_USER_BASE_URL}> . -EOF -) \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-generate.sh b/http-tests/system/end-user/POST-generate.sh deleted file mode 100755 index 715b671dba..0000000000 --- a/http-tests/system/end-user/POST-generate.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/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 writers group - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# create a parent container to generate into - -slug=$(uuidgen | tr '[:upper:]' '[:lower:]') - -parent=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Generate parent" \ - --slug "$slug" \ - --parent "$END_USER_BASE_URL") - -# POST /generate with a writer: generate a container for dh:Container class using ldh:SelectChildren query - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -@prefix void: . -@prefix spin: . -@prefix dh: . -@prefix ldh: . - -[] sioc:has_parent <${parent}> ; - void:class dh:Container ; - spin:query ldh:SelectChildren . -EOF -) \ -| grep -q "$STATUS_OK" diff --git a/pom.xml b/pom.xml index d858c74cdd..530bd9fe47 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.7.1 + 5.7.2-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub @@ -46,7 +46,7 @@ https://github.com/AtomGraph/LinkedDataHub scm:git:git://github.com/AtomGraph/LinkedDataHub.git scm:git:git@github.com:AtomGraph/LinkedDataHub.git - linkeddatahub-5.7.1 + linkeddatahub-5.5.4 diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 9147b37fc2..b83be580b8 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -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; @@ -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; @@ -288,6 +290,7 @@ public class Application extends ResourceConfig private final KeyStore keyStore, trustStore; private final URI secretaryWebIDURI; private final List supportedLanguages; + private final ExpiringMap 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 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 oidcModelCache = ExpiringMap.builder().variableExpiration().build(); private final ExpiringMap 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 @@ -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 getOntologyGraphs() + { + return ontologyGraphs; + } + /** * Returns a registry of readable and writeable media types. - * + * * @return registry object */ public MediaTypes getMediaTypes() diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java deleted file mode 100644 index 9bc6b93ad2..0000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright 2021 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.linkeddatahub.resource; - -import com.atomgraph.core.MediaTypes; -import com.atomgraph.core.vocabulary.SD; -import com.atomgraph.linkeddatahub.client.GraphStoreClient; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.Optional; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.StreamingOutput; -import jakarta.ws.rs.core.UriInfo; -import jakarta.ws.rs.ext.Providers; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ResIterator; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.DCTerms; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS endpoint for adding RDF data. - * - * @author {@literal Martynas Jusevičius } - */ -public class Add -{ - - private static final Logger log = LoggerFactory.getLogger(Add.class); - - private final UriInfo uriInfo; - private final MediaTypes mediaTypes; - private final Optional agentContext; - private final com.atomgraph.linkeddatahub.Application system; - - /** - * Constructs endpoint for synchronous RDF data imports. - * - * @param request current request - * @param uriInfo current URI info - * @param mediaTypes supported media types - * @param providers JAX-RS providers - * @param system system application - * @param agentContext authenticated agent's context - */ - @Inject - public Add(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - Optional agentContext, - @Context Providers providers, com.atomgraph.linkeddatahub.Application system) - { - this.uriInfo = uriInfo; - this.mediaTypes = mediaTypes; - this.agentContext = agentContext; - this.system = system; - } - - /** - * Adds RDF data from a remote source to a named graph. - * Expects a model containing a resource with dct:source (source URI) and sd:name (target graph URI) properties. - * - * @param model the RDF model containing the import parameters - * @return JAX-RS response with the imported data - */ - @POST - public Response post(Model model) - { - ResIterator it = model.listSubjectsWithProperty(DCTerms.source); - try - { - if (!it.hasNext()) throw new BadRequestException("Argument resource not provided"); - - Resource arg = it.next(); - Resource source = arg.getPropertyResourceValue(DCTerms.source); - if (source == null) throw new BadRequestException("RDF source URI (dct:source) not provided"); - - Resource graph = arg.getPropertyResourceValue(SD.name); - if (graph == null || !graph.isURIResource()) throw new BadRequestException("Graph URI (sd:name) not provided"); - - GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getMediaTypes()); // TO-DO: inject - Model importModel = gsc.getModel(source.getURI()); - // forward the stream to the named graph document -- do not directly append triples to graph because the agent might not have access to it - return forwardPost(Entity.entity(importModel, com.atomgraph.client.MediaType.APPLICATION_NTRIPLES_TYPE), graph.getURI()); - } - finally - { - it.close(); - } - } - - /** - * Forwards POST request to a graph. - * - * @param entity request entity - * @param graphURI the graph URI - * @return JAX-RS response - */ - protected Response forwardPost(Entity entity, String graphURI) - { - GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()). - delegation(getUriInfo().getBaseUri(), getAgentContext().orElse(null)); - // forward the stream to the named graph document. Buffer the entity first so that the server response is not returned before the client response completes - try (Response response = gsc.post(URI.create(graphURI), entity, gsc.getReadableMediaTypes(Model.class))) - { - return Response.status(response.getStatus()). - entity(response.readEntity(Model.class)). - build(); - } - } - - /** - * Converts input stream to streaming output. - * @param is input stream - * @return streaming output - */ - public StreamingOutput getStreamingOutput(InputStream is) - { - return (OutputStream os) -> { - is.transferTo(os); - }; - } - - /** - * Returns the supported media types. - * - * @return media types - */ - public MediaTypes getMediaTypes() - { - return mediaTypes; - } - - /** - * Returns the current URI info. - * - * @return URI info - */ - public UriInfo getUriInfo() - { - return uriInfo; - } - - /** - * Returns the authenticated agent's context. - * - * @return optional agent context - */ - public Optional getAgentContext() - { - return agentContext; - } - - /** - * Returns the system application. - * - * @return system application - */ - public com.atomgraph.linkeddatahub.Application getSystem() - { - return system; - } - -} diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java deleted file mode 100644 index 27e2124ffc..0000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java +++ /dev/null @@ -1,313 +0,0 @@ -/** - * Copyright 2022 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.linkeddatahub.resource; - -import com.atomgraph.core.MediaTypes; -import com.atomgraph.linkeddatahub.apps.model.Application; -import com.atomgraph.linkeddatahub.server.model.impl.DocumentHierarchyGraphStoreImpl; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import com.atomgraph.linkeddatahub.server.util.Skolemizer; -import com.atomgraph.linkeddatahub.vocabulary.LDH; -import com.atomgraph.linkeddatahub.vocabulary.VoID; -import com.atomgraph.linkeddatahub.vocabulary.DH; -import com.atomgraph.linkeddatahub.vocabulary.SIOC; -import com.atomgraph.spinrdf.vocabulary.SP; -import com.atomgraph.spinrdf.vocabulary.SPIN; -import java.net.URI; -import java.util.Calendar; -import java.util.Optional; -import java.util.UUID; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.InternalServerErrorException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.container.ResourceContext; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriBuilder; -import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.ontapi.model.OntModel; -import org.apache.jena.query.ParameterizedSparqlString; -import org.apache.jena.query.Query; -import org.apache.jena.query.QueryFactory; -import org.apache.jena.query.Syntax; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.ResIterator; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.DCTerms; -import org.apache.jena.vocabulary.RDF; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS resource that generates containers for given classes. - * - * @author {@literal Martynas Jusevičius } - */ -public class Generate -{ - - private static final Logger log = LoggerFactory.getLogger(Generate.class); - - private final UriInfo uriInfo; - private final MediaTypes mediaTypes; - private final Application application; - private final OntModel ontology; - private final Optional agentContext; - private final com.atomgraph.linkeddatahub.Application system; - private final ResourceContext resourceContext; - - /** - * Constructs endpoint for container generation. - * - * @param request current request - * @param uriInfo current URI info - * @param mediaTypes supported media types - * @param application matched application - * @param ontology ontology of the current application - * @param system system application - * @param agentContext authenticated agent's context - * @param resourceContext resource context for creating resources - */ - @Inject - public Generate(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional agentContext, - com.atomgraph.linkeddatahub.Application system, @Context ResourceContext resourceContext) - { - if (ontology.isEmpty()) throw new InternalServerErrorException("Ontology is not specified"); - this.uriInfo = uriInfo; - this.mediaTypes = mediaTypes; - this.application = application; - this.ontology = ontology.get(); - this.agentContext = agentContext; - this.system = system; - this.resourceContext = resourceContext; - } - - /** - * Generates containers for given classes. - * Expects a model containing a parent container (sioc:has_parent) and one or more class specifications - * with void:class and spin:query properties. Creates a new container for each class with a view based - * on the provided SPARQL SELECT query. - * - * @param model the RDF model containing the generation parameters - * @return JAX-RS response indicating success or failure - */ - @POST - public Response post(Model model) - { - ResIterator it = model.listSubjectsWithProperty(SIOC.HAS_PARENT); - try - { - if (!it.hasNext()) throw new BadRequestException("Argument resource not provided"); - - Resource arg = it.next(); - Resource service = arg.getPropertyResourceValue(LDH.service); - Resource parent = arg.getPropertyResourceValue(SIOC.HAS_PARENT); - if (parent == null) throw new BadRequestException("Parent container (sioc:has_parent) not provided"); - - ResIterator partIt = model.listSubjectsWithProperty(VoID._class); - try - { - while (partIt.hasNext()) - { - Resource part = partIt.next(); - Resource cls = part.getPropertyResourceValue(VoID._class); - Resource queryRes = part.getPropertyResourceValue(SPIN.query); - if (queryRes == null) throw new BadRequestException("Container query string (spin:query) not provided"); - - // Lookup query in ontology - Resource queryResource = getOntology().getResource(queryRes.getURI()); - if (queryResource == null || !queryResource.hasProperty(SP.text)) - throw new BadRequestException("Query resource not found in ontology: " + queryRes.getURI()); - - String queryString = queryResource.getProperty(SP.text).getString(); - Query query = QueryFactory.create(queryString, Syntax.syntaxARQ); - if (!query.isSelectType()) throw new BadRequestException("Container query is not of SELECT type"); - - ParameterizedSparqlString pss = new ParameterizedSparqlString(query.toString()); - pss.setIri(RDF.type.getLocalName(), cls.getURI()); // inject $type value - - URI containerGraphURI = UriBuilder.fromUri(parent.getURI()).path("{slug}/").build(UUID.randomUUID().toString()); - Model containerModel = ModelFactory.createDefaultModel(); - - createContainer(containerModel, - containerGraphURI, parent, - cls.getLocalName() + "s", - createView(containerModel, createContainerSelect(containerModel, - "Select " + cls.getLocalName(), - pss.asQuery(), - service))); - new Skolemizer(containerGraphURI.toString()).apply(containerModel); - - // append triples directly to the graph store without doing an HTTP request (and thus no ACL check) - try (Response containerResponse = getResourceContext().getResource(DocumentHierarchyGraphStoreImpl.class).post(containerModel, false, containerGraphURI)) - { - if (!containerResponse.getStatusInfo().getFamily().equals(Status.Family.SUCCESSFUL)) - { - if (log.isErrorEnabled()) log.error("Cannot create container"); - throw new InternalServerErrorException("Cannot create container"); - } - } - } - } - finally - { - partIt.close(); - } - - // ban the parent container URI from proxy cache to make sure the next query using it will be fresh (e.g. SELECT that loads children) - getSystem().ban(getSystem().getServiceContext(getApplication().getService()).getBackendProxy(), parent.getURI(), true); - - return Response.ok().build(); - } - finally - { - it.close(); - } - } - - /** - * Creates SELECT SPARQL query. - * - * @param model RDF model - * @param title query title - * @param query query object - * @param service optional SPARQL service resource - * @return query resource - */ - public Resource createContainerSelect(Model model, String title, Query query, Resource service) - { - Resource resource = model.createResource(). - addProperty(RDF.type, SP.Select). - addLiteral(DCTerms.title, title). - addProperty(SP.text, query.toString()); - - if (service != null) resource.addProperty(LDH.service, service); - - return resource; - } - - /** - * Creates a container document. - * - * @param model RDF model - * @param graphURI named graph URI - * @param parent parent document resource - * @param title document title - * @param content document content - * @return container resource - */ - public Resource createContainer(Model model, URI graphURI, Resource parent, String title, Resource content) - { - return model.createResource(graphURI.toString()). - addProperty(RDF.type, DH.Container). - addProperty(SIOC.HAS_PARENT, parent). - addLiteral(DCTerms.title, title). - addLiteral(DH.slug, UUID.randomUUID().toString()). - addLiteral(DCTerms.created, Calendar.getInstance()). - addProperty(model.createProperty(RDF.getURI(), "_1"), content); // TO-DO: make sure we're creating sequence value larger than the existing ones? - } - - /** - * Creates content resource. - * - * @param model RDF model - * @param query query resource - * @return content resource - */ - public Resource createView(Model model, Resource query) - { - return model.createResource(). - addProperty(RDF.type, LDH.View). - addProperty(SPIN.query, query); - } - - /** - * Returns the supported media types. - * - * @return media types - */ - public MediaTypes getMediaTypes() - { - return mediaTypes; - } - - /** - * Returns the current application. - * - * @return the application - */ - public Application getApplication() - { - return application; - } - - /** - * Returns the ontology. - * - * @return the ontology - */ - public OntModel getOntology() - { - return ontology; - } - - /** - * Returns the current URI info. - * - * @return URI info - */ - public UriInfo getUriInfo() - { - return uriInfo; - } - - /** - * Returns the authenticated agent's context. - * - * @return optional agent context - */ - public Optional getAgentContext() - { - return agentContext; - } - - /** - * Returns the system application. - * - * @return system application - */ - public com.atomgraph.linkeddatahub.Application getSystem() - { - return system; - } - - /** - * Returns the resource context. - * - * @return resource context - */ - public ResourceContext getResourceContext() - { - return resourceContext; - } - -} \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java index 5ce68929e5..50cd157ca9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java @@ -140,9 +140,9 @@ public Response get(@QueryParam(QUERY) Query query, // the application ontology MUST use a 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"); diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java index 9ce40f1a68..190fbd3467 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java @@ -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(); @@ -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(); diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java index e9bd7af715..319cf68e3e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java @@ -19,14 +19,13 @@ import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.client.util.jena.PrefixGraphRepository; +import com.atomgraph.linkeddatahub.server.util.ScopedGraphRepository; import com.atomgraph.linkeddatahub.vocabulary.LAPP; import com.atomgraph.server.exception.OntologyException; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.HashSet; import java.util.Optional; -import java.util.Set; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; @@ -34,12 +33,13 @@ import jakarta.ws.rs.container.PreMatching; import org.apache.jena.ontapi.OntModelFactory; import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.UnionGraph; import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.vocabulary.OWL; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; +import org.apache.jena.vocabulary.OWL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,8 +131,9 @@ public OntModel getOntology(Application app) } /** - * Loads the ontology model for the specified ontology URI, building its owl:imports closure with - * RDFS inference and materializing the inferences into the repository cache. + * Returns the ontology model for the specified ontology URI, assembling its owl:imports closure + * on a cache miss. The returned model is a fresh per-request wrapper over the shared closure + * union graph. * * @param app application resource * @param uri ontology URI @@ -146,64 +147,58 @@ public OntModel getOntology(Application app, String uri) final PrefixGraphRepository repository = app.canAs(EndUserApplication.class) ? getSystem().getRepository(app.as(EndUserApplication.class)) : getSystem().getRepository(); - // only build the materialized model if the ontology is not already cached; the double check under the - // repository lock ensures a single thread materializes it (loadOntology is a compound load + inference + - // put, not atomic), so concurrent cold requests don't duplicate the work or race each other's writes - if (!repository.isCached(uri)) + // only assemble the closure if it is not already cached; the double check under the repository + // lock ensures a single thread assembles it (loadOntology is a compound load + union build, not + // atomic), so concurrent cold requests don't duplicate the work or race each other's writes + UnionGraph union = getSystem().getOntologyGraphs().get(uri); + if (union == null) { synchronized (repository) { - if (!repository.isCached(uri)) loadOntology(repository, uri); + union = getSystem().getOntologyGraphs().get(uri); + if (union == null) + { + union = loadOntology(repository, uri); + getSystem().getOntologyGraphs().put(uri, union); + } } } - return OntModelFactory.createModel(repository.get(uri), OntSpecification.OWL2_FULL_MEM); + return OntModelFactory.createModel(union, OntSpecification.OWL2_FULL_MEM); } /** - * Builds and caches the materialized ontology model. Assembles the owl:imports closure into a single - * graph (so ontapi never manages a union-graph hierarchy over the shared repository), applies RDFS - * inference over the flattened closure, and materializes the inferences into the repository cache so - * the rules engine is not invoked on every request. + * Assembles the ontology's owl:imports closure as a union graph. ontapi resolves the closure through + * a scoped repository view: raw per-document graphs are read through (and cached in) the shared + * repository, while ontapi's union-graph bookkeeping stays local to the view — the shared repository + * keeps serving raw document graphs, and duplicate ontology IDs across applications cannot collide. + * No inference is applied: all consumers traverse class/property hierarchies explicitly. * * @param repository graph repository * @param uri ontology URI + * @return closure union graph */ - public static void loadOntology(PrefixGraphRepository repository, String uri) + public static UnionGraph loadOntology(PrefixGraphRepository repository, String uri) { if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}'", uri); - Model union = ModelFactory.createDefaultModel(); - Set closure = new HashSet<>(); - loadClosure(repository, uri, union, closure); - OntModel inferred = OntModelFactory.createModel(union.getGraph(), OntSpecification.OWL2_FULL_MEM_RDFS_INF); - OntModel materialized = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); - materialized.add(inferred); - // promote rdfs:Class to owl:Class so OWL2 profiles recognise third-party vocab terms (e.g. sp:Describe in sp.ttl) - inferred.listSubjectsWithProperty(RDF.type, RDFS.Class).forEach(r -> materialized.add(r, RDF.type, OWL.Class)); - repository.put(uri, materialized.getGraph()); - // cache imported graphs under their fragment-stripped document URIs too - closure.stream().filter(closureURI -> !closureURI.equals(uri)).forEach(importURI -> addDocumentModel(repository, importURI)); + ScopedGraphRepository scoped = new ScopedGraphRepository(repository); + OntModel ontology = OntModelFactory.createModel(repository.get(uri), OntSpecification.OWL2_FULL_MEM, scoped); + UnionGraph union = (UnionGraph)ontology.getGraph(); + // promote rdfs:Class to owl:Class so the OWL2 profile recognises third-party vocab terms (e.g. sp:Describe + // in sp.ttl) as named classes. The promotions live in their own union member so no document graph is + // polluted; carrying no owl:Ontology header, the member is ignored by ontapi's union-graph listener + Model promotions = ModelFactory.createDefaultModel(); + ontology.listSubjectsWithProperty(RDF.type, RDFS.Class).forEach(r -> promotions.add(r, RDF.type, OWL.Class)); + if (!promotions.isEmpty()) union.addSubGraph(promotions.getGraph()); + // cache closure graphs under their fragment-stripped document URIs too. ontapi keys imports under + // their declared ontology IRIs, which need not be repository entries (a content-addressed upload is + // cached under its uploads/ URI while declaring a foreign ontology IRI) — only alias ids the shared + // repository actually holds, lest the lookup dereference a foreign IRI over HTTP + scoped.ids().filter(closureURI -> closureURI.startsWith("http://") || closureURI.startsWith("https://")). + filter(repository::isCached). + forEach(closureURI -> addDocumentModel(repository, closureURI)); if (log.isDebugEnabled()) log.debug("Finished loading ontology with URI '{}'", uri); - } - - /** - * Recursively loads the transitive owl:imports closure of an ontology into a single union model, - * fetching each graph via the repository (SPARQL-first / bundled mappings). - * - * @param repository graph repository - * @param uri ontology URI - * @param union accumulator model - * @param seen accumulator of visited URIs (prevents cycles) - */ - public static void loadClosure(PrefixGraphRepository repository, String uri, Model union, Set seen) - { - if (!seen.add(uri)) return; - Model model = ModelFactory.createModelForGraph(repository.get(uri)); - union.add(model); - model.listObjectsOfProperty(OWL.imports).toList().forEach(imp -> - { - if (imp.isURIResource()) loadClosure(repository, imp.asResource().getURI(), union, seen); - }); + return union; } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java index 9166b28ea9..da1050384e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java @@ -22,7 +22,6 @@ import com.atomgraph.core.exception.BadGatewayException; import com.atomgraph.core.util.ModelUtils; import com.atomgraph.linkeddatahub.apps.model.Dataset; -import org.apache.jena.ontapi.model.OntModel; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.client.filter.auth.IDTokenDelegationFilter; import com.atomgraph.linkeddatahub.client.filter.auth.WebIDDelegationFilter; @@ -38,8 +37,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import org.apache.jena.query.ParameterizedSparqlString; -import org.apache.jena.query.QueryExecution; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.NotAllowedException; @@ -133,7 +130,6 @@ public class ProxyRequestFilter implements ContainerRequestFilter "Age"); @Inject com.atomgraph.linkeddatahub.Application system; - @Inject jakarta.inject.Provider> ontology; @Inject MediaTypes mediaTypes; @Context Request request; @@ -186,27 +182,6 @@ public void filter(ContainerRequestContext requestContext) throws IOException return; } - // serve terms from the app's in-memory namespace ontology (full imports closure) via DESCRIBE. - // covers both slash-based term URIs (e.g. schema:category) and hash-based namespaces - // (e.g. sioc:UserAccount → ac:document-uri strips to sioc:ns, so we also describe all - // ?term where STR(?term) starts with "#") - if (isSafeMethod && getOntology().isPresent()) - { - ParameterizedSparqlString pss = new ParameterizedSparqlString( - "DESCRIBE ?doc ?term WHERE { ?term ?p ?o FILTER(STRSTARTS(STR(?term), CONCAT(STR(?doc), \"#\"))) }"); - pss.setIri("doc", targetURI.toString()); - try (QueryExecution qe = QueryExecution.create(pss.asQuery(), getOntology().get())) - { - Model description = qe.execDescribe(); - if (!description.isEmpty()) - { - if (log.isDebugEnabled()) log.debug("Serving URI from namespace ontology: {}", targetURI); - requestContext.abortWith(getResponse(description, Response.Status.OK)); - return; - } - } - } - boolean isRegisteredApp = getSystem().matchApp(targetURI) != null; if (!isRegisteredApp && !getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled"); @@ -419,7 +394,7 @@ private Response overlayHeaders(Response response, Response clientResponse, bool /** * Builds a response for the given RDF model with type-appropriate content negotiation. - * Used for locally-served responses (DataManager cache, namespace ontology DESCRIBE) and for + * Used for locally-served responses (DataManager cache) and for * the proxy's Model branch. * * @param model RDF model @@ -481,16 +456,6 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - /** - * Returns the current application's namespace ontology, if available. - * - * @return optional ontology - */ - public Optional getOntology() - { - return ontology.get(); - } - /** * Returns the media types registry used for content negotiation and outbound {@code Accept} headers. * diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java b/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java index 018bca6c29..757c86bc1c 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java @@ -239,8 +239,9 @@ public Resource processRead(Resource resource) // this logic really belongs in a if (getApplication().isPresent() && getApplication().get().canAs(AdminApplication.class) && resource.hasProperty(RDF.type, OWL.Ontology)) { - // clear cached OntModel if ontology is updated. TO-DO: send event instead + // clear cached raw graph and closure union graph if ontology is updated. TO-DO: send event instead getSystem().getRepository().remove(resource.getURI()); + getSystem().getOntologyGraphs().remove(resource.getURI()); } if (getApplication().isPresent() && resource.hasProperty(RDF.type, ACL.Authorization)) diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java index 670c8a0d21..bfbc2562bb 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java @@ -16,8 +16,6 @@ */ package com.atomgraph.linkeddatahub.server.model.impl; -import com.atomgraph.linkeddatahub.resource.Add; -import com.atomgraph.linkeddatahub.resource.Generate; import com.atomgraph.linkeddatahub.resource.Namespace; import com.atomgraph.linkeddatahub.resource.Transform; import com.atomgraph.linkeddatahub.resource.admin.ClearOntology; @@ -134,17 +132,6 @@ public Class getFileItem() return com.atomgraph.linkeddatahub.resource.upload.Item.class; } - /** - * Returns the endpoint for synchronous RDF imports. - * - * @return endpoint resource - */ - @Path("add") - public Class getAddEndpoint() - { - return Add.class; - } - /** * Returns the endpoint for synchronous RDF imports with a CONSTRUCT query transformation. * @@ -156,17 +143,6 @@ public Class getTransformEndpoint() return Transform.class; } - /** - * Returns the endpoint for container generation. - * - * @return endpoint resource - */ - @Path("generate") - public Class getGenerateEndpoint() - { - return Generate.class; - } - /** * Returns the endpoint that allows clearing ontologies from cache by URI. * diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/ScopedGraphRepository.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/ScopedGraphRepository.java new file mode 100644 index 0000000000..4fec524ed2 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/ScopedGraphRepository.java @@ -0,0 +1,133 @@ +/** + * 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.linkeddatahub.server.util; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.jena.graph.Graph; +import org.apache.jena.ontapi.GraphRepository; + +/** + * A graph repository view that reads through a shared backing repository but keeps writes local. + *

+ * Handed to {@code OntModelFactory.createModel(Graph, OntSpecification, GraphRepository)} so that + * ontapi's union-graph bookkeeping — a {@code UnionGraph} wrapper per ontology in the imports + * closure, with listeners — lands in this instance's private store instead of the shared repository. + * The shared repository must keep answering {@code get(uri)} with the raw per-document graph (that is + * what proxied and direct document GETs serve), and duplicate ontology IDs across applications must + * not collide in one store. Reads fall through to the backing repository, triggering its + * SPARQL-first/mapped/HTTP loading and raw caching as usual, so resolving an imports closure through + * this view populates the shared raw cache as a side effect. + *

+ * After model construction, {@link #ids()} equals the set of resolved closure ontology IDs. + * + * @author Martynas Jusevičius {@literal } + */ +public class ScopedGraphRepository implements GraphRepository +{ + + private final GraphRepository backing; + private final Map local = new HashMap<>(); + + /** + * Constructs the view over a shared backing repository. + * + * @param backing shared graph repository + */ + public ScopedGraphRepository(GraphRepository backing) + { + this.backing = backing; + } + + @Override + public Graph get(String id) + { + Graph graph = local.get(id); + if (graph != null) return graph; + + return getBacking().get(id); + } + + @Override + public Stream ids() + { + return List.copyOf(local.keySet()).stream(); + } + + @Override + public Graph put(String id, Graph graph) + { + return local.put(id, graph); + } + + @Override + public Graph remove(String id) + { + return local.remove(id); + } + + @Override + public void clear() + { + local.clear(); + } + + @Override + public boolean contains(String id) + { + if (local.containsKey(id) || getBacking().contains(id)) return true; + + // the backing repository's contains() only reports already-cached graphs, but ontapi consults + // contains() before get() when resolving imports — a false negative for a resolvable id (bundled + // mapping, SPARQL-first, HTTP) makes ontapi silently substitute an empty ontology graph for the + // import. Attempt resolution instead: the backing repository loads and caches the graph, and only + // a genuinely unresolvable id reports absent + try + { + return getBacking().get(id) != null; + } + catch (RuntimeException ex) + { + return false; + } + } + + @Override + public long count() + { + return local.size(); + } + + @Override + public Stream graphs() + { + return List.copyOf(local.values()).stream(); + } + + /** + * Returns the shared backing repository. + * + * @return graph repository + */ + public GraphRepository getBacking() + { + return backing; + } + +} diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl index c309ecd423..bc95f3ea61 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl @@ -158,6 +158,10 @@ exclude-result-prefixes="#all" ixsl:then(ldh:http-request-threaded(?, 'metadata-request', 'metadata-response')) => ixsl:then(ldh:handle-response(?, 'metadata-response')) => ixsl:then(ldh:set-object-metadata#1) => + ixsl:then(ldh:load-property-metadata(?, 'view-results-response')) => + ixsl:then(ldh:http-request-threaded(?, 'property-metadata-request', 'property-metadata-response')) => + ixsl:then(ldh:handle-response(?, 'property-metadata-response')) => + ixsl:then(ldh:set-property-metadata#1) => ixsl:then(ldh:render-view#1) "/> @@ -781,6 +785,7 @@ exclude-result-prefixes="#all" + @@ -822,18 +827,33 @@ exclude-result-prefixes="#all" - + - - - + + @@ -865,31 +885,6 @@ exclude-result-prefixes="#all"

- - - - - - - - - - - - - @@ -939,6 +934,7 @@ exclude-result-prefixes="#all" + @@ -965,21 +961,28 @@ exclude-result-prefixes="#all" - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -1845,6 +1848,7 @@ exclude-result-prefixes="#all" + @@ -1920,6 +1924,7 @@ exclude-result-prefixes="#all" + @@ -1929,6 +1934,7 @@ exclude-result-prefixes="#all" + @@ -1954,45 +1960,6 @@ exclude-result-prefixes="#all" - - - - - - - - - - ldh:facet-filter-response - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2010,8 +1977,9 @@ exclude-result-prefixes="#all" - - + + + + ), map{ 'duplicates': 'use-last' })"/> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl index 82dc364e41..928169c8a1 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl @@ -40,6 +40,9 @@ xmlns:ldt="&ldt;" xmlns:sd="&sd;" xmlns:sioc="&sioc;" xmlns:dct="&dct;" +xmlns:dh="&dh;" +xmlns:sp="&sp;" +xmlns:spin="&spin;" xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" extension-element-prefixes="ixsl" exclude-result-prefixes="#all" @@ -82,7 +85,7 @@ LIMIT 10 - + @@ -101,7 +104,10 @@ LIMIT 10 @@ -210,9 +216,7 @@ LIMIT 10 - -