From 929246370394bf1f79ec7f2f03353d09fa0c51ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 10 Jun 2026 00:05:50 +0200 Subject: [PATCH 01/30] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index df0f49498..bcdd7851c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.5.3 + 5.5.4-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.5.3 + linkeddatahub-2.1.1 From ff479c32e4f2e4d48395fbc3256b5c46c76eb9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 17 Jun 2026 15:20:35 +0200 Subject: [PATCH 02/30] Add regression test for Varnish cache poisoning via On-Behalf-Of delegation Proxy server-side fetches set On-Behalf-Of (WebIDDelegationFilter), and the backend response carries the asserted agent's WebID in the Link header. The URL-keyed varnish-frontend entry then replays that response to subsequent anonymous requests for the same URL+Accept. Test fires the proxy request as owner, then asserts that an anonymous direct request returns 403 and that no acl#agent appears in the Link header. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../proxy/GET-proxied-no-cache-poisoning.sh | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100755 http-tests/proxy/GET-proxied-no-cache-poisoning.sh diff --git a/http-tests/proxy/GET-proxied-no-cache-poisoning.sh b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh new file mode 100755 index 000000000..e1467c34f --- /dev/null +++ b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Regression: ProxyRequestFilter's server-side fetch attaches On-Behalf-Of +# (via WebIDDelegationFilter), and the backend response carries the asserted +# agent's WebID in the Link header (acl#agent). varnish-frontend must not +# cache that response under a URL-keyed entry — otherwise a subsequent +# anonymous request to the same URL+Accept replays the cached 200 and reads +# back the previous agent's identity (and inherits whatever ACL grant they +# had). + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# Step A: authenticated owner fires a proxy request from the end-user +# dataspace to the admin dataspace. This triggers WebIDDelegationFilter → +# On-Behalf-Of on the server-side hop into varnish-frontend. + +curl -k -f -s -o /dev/null \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -G \ + -H 'Accept: application/rdf+xml' \ + --data-urlencode "uri=${ADMIN_BASE_URL}" \ + "${END_USER_BASE_URL}" + +# Step B: anonymous direct request to the admin URL with the same Accept. +# If the cache was poisoned in Step A, this returns 200 with the owner's +# WebID in the Link header. Expected after the fix: varnish-frontend should +# pass on On-Behalf-Of and store nothing, so this goes to the backend +# anonymously and gets 403. + +response=$(curl -k -s -i -H 'Accept: application/rdf+xml' "${ADMIN_BASE_URL}") + +status=$(printf '%s\n' "$response" | awk 'NR==1{print $2}' | tr -d '\r') +link_leak=$(printf '%s\n' "$response" | tr -d '\r' | grep -i '^link:' | grep -c 'acl#agent' || true) + +if [ "$status" != "$STATUS_FORBIDDEN" ]; then + echo "Expected $STATUS_FORBIDDEN anonymous, got: $status" + printf '%s\n' "$response" | head -40 + exit 1 +fi + +if [ "$link_leak" != "0" ]; then + echo "Anonymous response leaks acl#agent in Link header (cache poisoning):" + printf '%s\n' "$response" | grep -i '^link:' + exit 1 +fi From d937d8cc95ba888ef9f547755a3ae0f7babb937a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 17 Jun 2026 15:25:32 +0200 Subject: [PATCH 03/30] Bypass varnish-frontend cache for On-Behalf-Of requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proxy server-side fetches set On-Behalf-Of (WebIDDelegationFilter), and the backend response echoes the asserted agent's WebID in the Link header (acl#agent). The previous vcl_recv only passed for Client-Cert + HTML, so the delegated RDF response was stored under a key that ignored the asserted identity — any subsequent anonymous request to the same URL+Accept replayed the cached 200 and read back the previous agent's identity and ACL grant. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 02b5f5203..71f82f818 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -384,6 +384,15 @@ configs: return (pass); } + # Delegated requests carry user identity in the On-Behalf-Of header. + # The backend response echoes that identity in the Link header + # (acl#agent). The cache key does not include the asserted identity, + # so caching would let a later anonymous request to the same + # URL+Accept read back the previous agent's WebID and ACL grant. + if (req.http.On-Behalf-Of) { + return (pass); + } + if (req.http.Client-Cert) { # Authenticated HTML is user-specific → never cache if (req.http.Accept ~ "text/html" || From 72804d72be0a019e6496f1061a5cfb98b5848792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 17 Jun 2026 15:28:14 +0200 Subject: [PATCH 04/30] Show progress cursor while 3D graph requests are in flight Stamp body.cursor=progress when ldh:http-request is issued for the document RDF and backlinks fetches, and reset to default when the corresponding response handlers run, so the user gets feedback that the 3D graph is loading. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl index bbfff9e59..3ccab5083 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl @@ -122,6 +122,8 @@ WHERE 'graph-state': $graph-state }" as="map(*)"/> + + + + + + @@ -381,6 +387,8 @@ WHERE + + From cd7dd018c59345dfd993eccf3ea75dec0bb855c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 17 Jun 2026 15:28:55 +0200 Subject: [PATCH 05/30] Defer 'active' token on new tab panes to ldh:ActivateTab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newly-appended tab-body elements were defaulting to class="tab-pane active", which briefly left two panes carrying the 'active' token (the new one and the currently-active local one) between append and ldh:ActivateTab. Code running in that window — notably ldh:LeftSidebar, which reads ldt:base() and sd:endpoint() off the active pane — would see ambiguous state and crash. Pass class="tab-pane" explicitly so only ldh:ActivateTab promotes a pane to active. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl index ecf8eff43..ddf605df9 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -559,8 +559,10 @@ WHERE + + @@ -718,6 +720,7 @@ WHERE + From 62656d74ce14b05c5d677e06e600e7b45f8e3be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 08:23:01 +0200 Subject: [PATCH 06/30] Extend cache-poisoning regression to the Client-Cert + RDF path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing Step A/B covered the proxy-delegation hop (On-Behalf-Of asserted in the HTTP header). It misses the symmetric path where the LDH server-side client presents its keystore cert at TLS to nginx: the backend stamps acl#agent into the Link header for the authenticated 200, and the URL-keyed cache slot ignores the asserter, so a later anonymous request reads the response back. Steps C/D drive that path directly — authenticated owner GET with Accept: application/rdf+xml, then anonymous direct GET, asserts 403 and no acl#agent in the Link header. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../proxy/GET-proxied-no-cache-poisoning.sh | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/http-tests/proxy/GET-proxied-no-cache-poisoning.sh b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh index e1467c34f..2d4c2d130 100755 --- a/http-tests/proxy/GET-proxied-no-cache-poisoning.sh +++ b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh @@ -46,3 +46,37 @@ if [ "$link_leak" != "0" ]; then printf '%s\n' "$response" | grep -i '^link:' exit 1 fi + +# Step C: authenticated owner fetches the admin URL directly with cert at TLS, +# Accept: application/rdf+xml. The Client-Cert header reaches varnish-frontend +# (nginx-forwarded). The backend stamps acl#agent into the Link header for the +# authenticated 200. varnish-frontend must NOT cache this response — its hash +# key ignores identity, so a subsequent anonymous request would replay the 200. + +purge_cache "$FRONTEND_VARNISH_SERVICE" + +curl -k -f -s -o /dev/null \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H 'Accept: application/rdf+xml' \ + "${ADMIN_BASE_URL}" + +# Step D: anonymous direct fetch of the same URL. With the fix in place +# (Client-Cert + non-/static/ path → pass in vcl_recv), Step C didn't store +# anything, so this reaches the backend anonymously and gets 403. + +response=$(curl -k -s -i -H 'Accept: application/rdf+xml' "${ADMIN_BASE_URL}") + +status=$(printf '%s\n' "$response" | awk 'NR==1{print $2}' | tr -d '\r') +link_leak=$(printf '%s\n' "$response" | tr -d '\r' | grep -i '^link:' | grep -c 'acl#agent' || true) + +if [ "$status" != "$STATUS_FORBIDDEN" ]; then + echo "[Client-Cert path] Expected $STATUS_FORBIDDEN anonymous, got: $status" + printf '%s\n' "$response" | head -40 + exit 1 +fi + +if [ "$link_leak" != "0" ]; then + echo "[Client-Cert path] Anonymous response leaks acl#agent in Link header (cache poisoning):" + printf '%s\n' "$response" | grep -i '^link:' + exit 1 +fi From 699bddd2c74079d66073515157ddbe5394235b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 08:36:25 +0200 Subject: [PATCH 07/30] Bypass varnish-frontend cache for any Client-Cert request outside /static/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous Client-Cert block only passed for HTML and a few specific endpoints. RDF/Turtle/JSON-LD/SPARQL-results responses fell through to hash and got stored even though the backend stamps acl#agent into the Link header for every authenticated response, leaking that identity (and the asserter's ACL grant) to any anonymous reader of the same URL. The replacement rule is the actual semantic boundary: /static/* is served by Tomcat's default servlet (web.xml:365-371), bypasses Jersey, carries no identity in headers, and is safe to share across cache callers. Every other path is potentially user-specific when the request authenticates, so it must pass. The previously-listed If-Match / /access / /acl/agents/ sub-clauses are subsumed by Client-Cert + !/static/ → pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 71f82f818..5f3522f1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -393,28 +393,15 @@ configs: return (pass); } - if (req.http.Client-Cert) { - # Authenticated HTML is user-specific → never cache - if (req.http.Accept ~ "text/html" || - req.http.Accept ~ "application/xhtml+xml") { - return (pass); - } - - # Conditional requests must reach backend for validation - if (req.http.If-Match || req.http.If-None-Match || - req.http.If-Modified-Since || req.http.If-Unmodified-Since) { - return (pass); - } - - # /access endpoint returns agent-specific group memberships - if (req.url ~ "^/access") { - return (pass); - } - - # SPARQL referencing /acl/agents/ depends on agent identity → don't cache - if (req.url ~ "%2Facl%2Fagents%2F") { - return (pass); - } + # Authenticated responses get acl#agent stamped into the Link header by + # the backend, regardless of representation (HTML, RDF/XML, Turtle, + # JSON-LD, SPARQL results, …). The URL-keyed cache slot ignores the + # asserting identity, so any such response would leak to anonymous + # readers of the same URL. /static/* is the only safely-shared path — + # it's served by Tomcat's default servlet (web.xml:365-371), bypasses + # Jersey, and carries no identity-bearing headers. + if (req.http.Client-Cert && req.url !~ "^/static/") { + return (pass); } if (req.http.Cookie) { From 5febdea770b1ef370676d0efa24beb22e118c864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 08:39:57 +0200 Subject: [PATCH 08/30] Trim cache-poisoning regression failure output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the response-body dumps and `[Client-Cert path]` tag prefixes — single echo + exit 1, matching the convention of sibling tests like GET-proxied-internal-403.sh. Step name still printed so a CI failure points at the right assertion. Co-Authored-By: Claude Opus 4.7 (1M context) --- http-tests/proxy/GET-proxied-no-cache-poisoning.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/http-tests/proxy/GET-proxied-no-cache-poisoning.sh b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh index 2d4c2d130..89337d238 100755 --- a/http-tests/proxy/GET-proxied-no-cache-poisoning.sh +++ b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh @@ -36,14 +36,12 @@ status=$(printf '%s\n' "$response" | awk 'NR==1{print $2}' | tr -d '\r') link_leak=$(printf '%s\n' "$response" | tr -d '\r' | grep -i '^link:' | grep -c 'acl#agent' || true) if [ "$status" != "$STATUS_FORBIDDEN" ]; then - echo "Expected $STATUS_FORBIDDEN anonymous, got: $status" - printf '%s\n' "$response" | head -40 + echo "Step B: expected $STATUS_FORBIDDEN, got $status" exit 1 fi if [ "$link_leak" != "0" ]; then - echo "Anonymous response leaks acl#agent in Link header (cache poisoning):" - printf '%s\n' "$response" | grep -i '^link:' + echo "Step B: anonymous response leaks acl#agent (cache poisoning)" exit 1 fi @@ -70,13 +68,11 @@ status=$(printf '%s\n' "$response" | awk 'NR==1{print $2}' | tr -d '\r') link_leak=$(printf '%s\n' "$response" | tr -d '\r' | grep -i '^link:' | grep -c 'acl#agent' || true) if [ "$status" != "$STATUS_FORBIDDEN" ]; then - echo "[Client-Cert path] Expected $STATUS_FORBIDDEN anonymous, got: $status" - printf '%s\n' "$response" | head -40 + echo "Step D: expected $STATUS_FORBIDDEN, got $status" exit 1 fi if [ "$link_leak" != "0" ]; then - echo "[Client-Cert path] Anonymous response leaks acl#agent in Link header (cache poisoning):" - printf '%s\n' "$response" | grep -i '^link:' + echo "Step D: anonymous response leaks acl#agent (cache poisoning)" exit 1 fi From 9ec3b671d7c95780faadf902cd940a6071bef61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 09:41:42 +0200 Subject: [PATCH 09/30] Forward upstream validators for HEAD proxy responses The typed branches in getResponse() parse the upstream entity into a Model/ResultSet and re-stamp ETag/Last-Modified off the parsed value. For HEAD responses the body is empty by HTTP semantics, so that parse yields an empty Model/ResultSet whose hash-derived ETag disagrees with the GET ETag for the same URL. Previously masked by varnish-frontend: a GET-populated cache entry served HEAD requests with the original ETag, so the proxy's HEAD upstream fetch read body bytes from cache and the re-stamped validator happened to match direct. After widening the Client-Cert bypass in vcl_recv, both fetches go fresh to backend and the proxy's empty-body parse is exercised for real, producing a wrong ETag. For HEAD, skip the typed branches entirely and forward the upstream's ETag/Last-Modified verbatim via overlayHeaders(copyValidators=true). The proxy's response now agrees with the direct response. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../filter/request/ProxyRequestFilter.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 e42e43a93..b2e56aa22 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 @@ -50,6 +50,7 @@ import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.EntityTag; @@ -240,7 +241,7 @@ else if (agentContext instanceof IDTokenSecurityContext idTokenSecurityContext) try (clientResponse) { - requestContext.abortWith(getResponse(clientResponse, targetURI)); + requestContext.abortWith(getResponse(clientResponse, targetURI, requestContext.getMethod())); } } catch (MessageBodyProviderNotFoundException ex) @@ -322,8 +323,20 @@ protected Optional resolveTargetURI(ContainerRequestContext requestContext) * @param targetURI upstream URI (used as the parse base URI hint for {@code ModelProvider}) * @return JAX-RS response to return to the original caller */ - protected Response getResponse(Response clientResponse, URI targetURI) + protected Response getResponse(Response clientResponse, URI targetURI, String method) { + // HEAD responses have no body by HTTP semantics. Routing them through + // the typed branches below would parse an empty entity into an empty + // Model/ResultSet, then re-stamp ETag/Last-Modified off that empty + // value — producing validators that disagree with the upstream GET. + // Forward the upstream headers (including ETag) verbatim instead. + if (HttpMethod.HEAD.equalsIgnoreCase(method)) + { + Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()); + if (clientResponse.getMediaType() != null) rb.type(clientResponse.getMediaType()); + return overlayHeaders(rb.build(), clientResponse, true); + } + if (clientResponse.getMediaType() == null) { Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()); From 2b2cc6700f25c66186c8d91ad0e68389e894f6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 09:59:05 +0200 Subject: [PATCH 10/30] Javadoc fix --- .../server/filter/request/ProxyRequestFilter.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 b2e56aa22..514a423f1 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 @@ -275,9 +275,7 @@ protected Optional resolveTargetURI(ContainerRequestContext requestContext) if (proxyTarget != null) return Optional.of(proxyTarget); // Case 2: lapp:Dataset proxy - @SuppressWarnings("unchecked") - Optional datasetOpt = - (Optional) requestContext.getProperty(LAPP.Dataset.getURI()); + Optional datasetOpt = (Optional) requestContext.getProperty(LAPP.Dataset.getURI()); if (datasetOpt != null && datasetOpt.isPresent()) { URI proxied = datasetOpt.get().getProxied(requestContext.getUriInfo().getAbsolutePath()); @@ -321,6 +319,7 @@ protected Optional resolveTargetURI(ContainerRequestContext requestContext) * * @param clientResponse response from the proxy target * @param targetURI upstream URI (used as the parse base URI hint for {@code ModelProvider}) + * @param method HTTP method * @return JAX-RS response to return to the original caller */ protected Response getResponse(Response clientResponse, URI targetURI, String method) From a6d71b55b9c6840f44d6190e0165a7b51b79b9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 10:37:04 +0200 Subject: [PATCH 11/30] Use ParameterizedSparqlString for proxy namespace DESCRIBE query Bind the target URI via setIri() instead of string-concatenating it into the query text, eliminating the SPARQL injection surface on a URI containing metacharacters. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../server/filter/request/ProxyRequestFilter.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 514a423f1..4eeb6599e 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 @@ -38,8 +38,8 @@ 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 org.apache.jena.query.QueryFactory; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.NotAllowedException; @@ -192,9 +192,10 @@ public void filter(ContainerRequestContext requestContext) throws IOException // ?term where STR(?term) starts with "#") if (isSafeMethod && getOntology().isPresent()) { - String describeQueryStr = "DESCRIBE <" + targetURI + "> ?term " + - "WHERE { ?term ?p ?o FILTER(STRSTARTS(STR(?term), CONCAT(STR(<" + targetURI + ">), \"#\"))) }"; - try (QueryExecution qe = QueryExecution.create(QueryFactory.create(describeQueryStr), getOntology().get().getOntModel())) + 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().getOntModel())) { Model description = qe.execDescribe(); if (!description.isEmpty()) From 1bff73940e896489f9b43e0d1c0a30ddea0a7f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 18 Jun 2026 10:54:07 +0200 Subject: [PATCH 12/30] Add unit tests for graph store, RDF import streaming, and proxied WebID auth DocumentHierarchyGraphStoreImplTest covers the stateless logic (getChangedResources, getLastModified, writeFile/writeFiles guards and file I/O) via a CALLS_REAL_METHODS mock, since the class only exposes a heavyweight @Inject constructor. StreamRDFOutputWriterTest covers the RDF-import streaming path: null guard, getter round-trip, and the non-RDF media type -> BadRequestException branch. The CSV transformation itself is covered upstream in CSV2RDF. ProxiedWebIDFilterTest covers Client-Cert header PEM parsing into an X509Certificate, absent header, and malformed input. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../stream/StreamRDFOutputWriterTest.java | 100 +++++++ .../request/auth/ProxiedWebIDFilterTest.java | 115 ++++++++ .../DocumentHierarchyGraphStoreImplTest.java | 274 ++++++++++++++++++ 3 files changed, 489 insertions(+) create mode 100644 src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java create mode 100644 src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java create mode 100644 src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java diff --git a/src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java b/src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java new file mode 100644 index 000000000..0e3f2b6cb --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java @@ -0,0 +1,100 @@ +/** + * Copyright 2025 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.imports.stream; + +import com.atomgraph.linkeddatahub.client.GraphStoreClient; +import com.atomgraph.linkeddatahub.model.Service; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link StreamRDFOutputWriter}. + * + * @author Martynas Jusevičius {@literal } + */ +@ExtendWith(MockitoExtension.class) +public class StreamRDFOutputWriterTest +{ + + @Mock private Service service; + @Mock private Service adminService; + @Mock private com.atomgraph.linkeddatahub.Application system; + @Mock private GraphStoreClient gsc; + + private static final String BASE_URI = "http://localhost/"; + private static final String GRAPH_URI = "http://localhost/graphs/import"; + + private Query query; + private StreamRDFOutputWriter writer; + + @BeforeEach + public void setUp() + { + query = QueryFactory.create("CONSTRUCT WHERE { ?s ?p ?o }"); + writer = new StreamRDFOutputWriter(service, adminService, system, gsc, BASE_URI, query, GRAPH_URI); + } + + @Test + public void testGettersRoundTrip() + { + assertSame(service, writer.getService()); + assertSame(adminService, writer.getAdminService()); + assertSame(system, writer.getSystem()); + assertSame(gsc, writer.getGraphStoreClient()); + assertEquals(BASE_URI, writer.getBaseURI()); + assertSame(query, writer.getQuery()); + assertEquals(GRAPH_URI, writer.getGraphURI()); + } + + @Test + public void testApplyNullResponse() + { + assertThrows(IllegalArgumentException.class, () -> writer.apply(null)); + } + + /** + * A response whose {@code Content-Type} does not map to an RDF language must be rejected with + * {@code 400 Bad Request} — after the body has been buffered to a temp file but before any + * Graph Store write is attempted. + */ + @Test + public void testApplyNonRdfMediaTypeThrowsBadRequest() + { + Response response = mock(Response.class); + lenient().when(response.readEntity(InputStream.class)).thenReturn((InputStream)new ByteArrayInputStream("not rdf".getBytes())); + when(response.getMediaType()).thenReturn(MediaType.valueOf("image/png")); + + assertThrows(BadRequestException.class, () -> writer.apply(response)); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java new file mode 100644 index 000000000..e23a32861 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java @@ -0,0 +1,115 @@ +/** + * Copyright 2025 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.filter.request.auth; + +import jakarta.ws.rs.container.ContainerRequestContext; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ProxiedWebIDFilter}, which extracts the client certificate from the + * {@code Client-Cert} request header (URL-encoded PEM) instead of the TLS connection. + * + * @author Martynas Jusevičius {@literal } + */ +@ExtendWith(MockitoExtension.class) +public class ProxiedWebIDFilterTest +{ + + private static final String CLIENT_CERT_HEADER_NAME = "Client-Cert"; + + /** A self-signed X.509 certificate (CN=Test WebID, O=LinkedDataHub) in PEM form. */ + private static final String CERT_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIC1jCCAb4CCQD35LqgLKP0ATANBgkqhkiG9w0BAQsFADAtMRMwEQYDVQQDDApU\n" + + "ZXN0IFdlYklEMRYwFAYDVQQKDA1MaW5rZWREYXRhSHViMB4XDTI2MDYxODA4Mzkz\n" + + "OFoXDTI2MDYxOTA4MzkzOFowLTETMBEGA1UEAwwKVGVzdCBXZWJJRDEWMBQGA1UE\n" + + "CgwNTGlua2VkRGF0YUh1YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" + + "AMRsMfh0IikmxDbZ0kca2FRI+WXelczdi+dU9ZC42cAZJM6pEu9icvCdTKBKipYE\n" + + "07PkfAgsmkS3qzly1iQzyXZFzopndg9FdFvWZyn8SdxNSKCcQt13NTp8sXflkdxK\n" + + "SfOseUx1cZ0T4ylGNwkqxcqZo5b06CJqiZqjgO4x7kYWWrli44AgzMkT3AgJqq5X\n" + + "iSo5j8gOjicR+ZywLAEvWH0ITja4sIgsQzZHxbquOuPEevnT+135M33wHxsY5MHJ\n" + + "Ykxid7C4ifVm4jXf81CmnoCifR9UeBnMZ0QBPBP/Exv+CpTgb3TBAfF1o1QsEuM3\n" + + "60fYmqLcwLiKgZqDJ7ZH80UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeYJGVnFq\n" + + "CARK15JQQk1YBAUPspkFWAeXH9UzYyxpqt0bLlYO9g4KExJVJvE9Qub2lHXBs36j\n" + + "/elRF+PR5Zt/6LD26OnSu+QWkFSqbO6Otul7g9ikMufuhNrZyyOOzidqFfcfkhWx\n" + + "FZh+yZhGoo2f+ddMuYbK3lKI+/DMswfdNN6VN++EOYskjWBB85GKUxEJTLEF2yE+\n" + + "yRtqnQfX3ucvO2Zd1XHsgknzoSfG8CXZF3GDcqzzEZ6Aa//xtwYRCmNmj9E9SdMY\n" + + "xuCHnQP3cV/vBBhxt1BWdIRtcU6xpasNMfWGgAxqrCTz+GnT7FExbe5qt6CgX7yl\n" + + "JLw8c9VNQzsM9g==\n" + + "-----END CERTIFICATE-----\n"; + + @Mock private ContainerRequestContext requestContext; + + private ProxiedWebIDFilter filter; + + @BeforeEach + public void setUp() + { + filter = new ProxiedWebIDFilter(); + } + + @Test + public void testCertificateFactoryIsX509() + { + assertNotNull(filter.getCertificateFactory()); + assertEquals("X.509", filter.getCertificateFactory().getType()); + } + + /** No {@code Client-Cert} header — no certificate. */ + @Test + public void testNoHeaderReturnsNull() throws Exception + { + when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn(null); + assertNull(filter.getWebIDCertificate(requestContext)); + } + + /** A URL-encoded PEM certificate in the header is decoded and parsed into an X509Certificate. */ + @Test + public void testValidHeaderParsesCertificate() throws Exception + { + String encoded = URLEncoder.encode(CERT_PEM, StandardCharsets.UTF_8); + when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn(encoded); + + X509Certificate cert = filter.getWebIDCertificate(requestContext); + + assertNotNull(cert); + assertTrue(cert.getSubjectX500Principal().getName().contains("Test WebID")); + } + + /** A header that is not a valid certificate must surface as a CertificateException. */ + @Test + public void testMalformedHeaderThrows() + { + when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn("garbage-not-a-cert"); + assertThrows(CertificateException.class, () -> filter.getWebIDCertificate(requestContext)); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java new file mode 100644 index 000000000..de95fa289 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java @@ -0,0 +1,274 @@ +/** + * Copyright 2025 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.model.impl; + +import com.atomgraph.linkeddatahub.vocabulary.SIOC; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Set; +import org.apache.jena.datatypes.xsd.XSDDateTime; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the stateless logic of {@link DocumentHierarchyGraphStoreImpl}. + *

+ * The class has a single heavyweight {@code @Inject} constructor that wires the JAX-RS request + * context and an HTTP-backed {@code GraphStoreClient}, so it cannot be instantiated directly in a + * unit test. The methods exercised here ({@code getChangedResources}, {@code getLastModified}, and + * the {@code writeFile}/{@code writeFiles} guards and file I/O) read no instance state, so we obtain + * an instance via Mockito's {@code CALLS_REAL_METHODS} (which skips the constructor) and invoke the + * real method bodies. + * + * @author Martynas Jusevičius {@literal } + */ +public class DocumentHierarchyGraphStoreImplTest +{ + + private final DocumentHierarchyGraphStoreImpl gs = mock(DocumentHierarchyGraphStoreImpl.class, CALLS_REAL_METHODS); + + // getChangedResources() + + @Test + public void testChangedResourcesNullBefore() + { + assertThrows(IllegalArgumentException.class, () -> gs.getChangedResources(null, ModelFactory.createDefaultModel())); + } + + @Test + public void testChangedResourcesNullAfter() + { + assertThrows(IllegalArgumentException.class, () -> gs.getChangedResources(ModelFactory.createDefaultModel(), null)); + } + + @Test + public void testChangedResourcesDetectsAddedSubject() + { + Model before = ModelFactory.createDefaultModel(); + Model after = ModelFactory.createDefaultModel(); + Resource added = after.createResource("http://localhost/added"); + added.addProperty(SIOC.HAS_PARENT, after.createResource("http://localhost/")); + + Set changed = gs.getChangedResources(before, after); + assertTrue(changed.contains(added)); + } + + @Test + public void testChangedResourcesDetectsRemovedSubject() + { + Model before = ModelFactory.createDefaultModel(); + Resource removed = before.createResource("http://localhost/removed"); + removed.addProperty(SIOC.HAS_PARENT, before.createResource("http://localhost/")); + Model after = ModelFactory.createDefaultModel(); + + Set changed = gs.getChangedResources(before, after); + assertTrue(changed.contains(removed)); + } + + @Test + public void testChangedResourcesIdenticalModelsAreEmpty() + { + Model before = ModelFactory.createDefaultModel(); + before.createResource("http://localhost/doc").addProperty(DCTerms.creator, before.createResource("http://localhost/agent")); + Model after = ModelFactory.createDefaultModel().add(before); + + assertTrue(gs.getChangedResources(before, after).isEmpty()); + } + + // getLastModified(Resource) + + @Test + public void testLastModifiedNullResource() + { + assertThrows(IllegalArgumentException.class, () -> gs.getLastModified((Resource)null)); + } + + @Test + public void testLastModifiedReturnsNullWhenNoDates() + { + Model model = ModelFactory.createDefaultModel(); + Resource resource = model.createResource("http://localhost/doc"); + assertNull(gs.getLastModified(resource)); + } + + @Test + public void testLastModifiedReturnsMaxOfCreatedAndModified() + { + Model model = ModelFactory.createDefaultModel(); + Calendar createdCal = new GregorianCalendar(2020, Calendar.JANUARY, 1, 0, 0, 0); + Calendar modifiedCal = new GregorianCalendar(2021, Calendar.JANUARY, 1, 0, 0, 0); + Resource resource = model.createResource("http://localhost/doc"). + addProperty(DCTerms.created, model.createTypedLiteral(createdCal)). + addProperty(DCTerms.modified, model.createTypedLiteral(modifiedCal)); + + // expected value is the later (modified) date, round-tripped through the same XSDDateTime path the method uses + Date expected = ((XSDDateTime)model.createTypedLiteral(modifiedCal).getValue()).asCalendar().getTime(); + assertEquals(expected, gs.getLastModified(resource)); + } + + @Test + public void testLastModifiedIgnoresNonDateTimeLiterals() + { + Model model = ModelFactory.createDefaultModel(); + Resource resource = model.createResource("http://localhost/doc"). + addProperty(DCTerms.modified, "not a date"); // plain string literal, not xsd:dateTime + assertNull(gs.getLastModified(resource)); + } + + // getLastModified(Model, URI) + + @Test + public void testLastModifiedByGraphURIReturnsNullForNullURI() + { + assertNull(gs.getLastModified(ModelFactory.createDefaultModel(), null)); + } + + @Test + public void testLastModifiedByGraphURI() + { + Model model = ModelFactory.createDefaultModel(); + Calendar cal = new GregorianCalendar(2022, Calendar.MARCH, 15, 12, 0, 0); + URI graphURI = URI.create("http://localhost/doc"); + model.createResource(graphURI.toString()).addProperty(DCTerms.modified, model.createTypedLiteral(cal)); + + Date expected = ((XSDDateTime)model.createTypedLiteral(cal).getValue()).asCalendar().getTime(); + assertEquals(expected, gs.getLastModified(model, graphURI)); + } + + // writeFile(File, InputStream) + + @Test + public void testWriteFileNullFile() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile((File)null, new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileNullInputStream(@TempDir Path tempDir) + { + File file = tempDir.resolve("out.bin").toFile(); + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(file, (InputStream)null)); + } + + @Test + public void testWriteFileWritesContent(@TempDir Path tempDir) throws Exception + { + File file = tempDir.resolve("out.bin").toFile(); + byte[] data = "hello world".getBytes(StandardCharsets.UTF_8); + + gs.writeFile(file, new ByteArrayInputStream(data)); + + assertTrue(file.exists()); + assertArrayEquals(data, Files.readAllBytes(file.toPath())); + } + + // writeFile(URI, URI, URI, InputStream) + + @Test + public void testWriteFileByURINullURI(@TempDir Path tempDir) + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(null, URI.create("http://localhost/"), tempDir.toUri(), new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURIRelativeURIRejected(@TempDir Path tempDir) + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(URI.create("relative/path"), URI.create("http://localhost/"), tempDir.toUri(), new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURINullBase(@TempDir Path tempDir) + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(URI.create("http://localhost/myfile"), null, tempDir.toUri(), new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURINullUploadRoot() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(URI.create("http://localhost/myfile"), URI.create("http://localhost/"), null, new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURIResolvesRelativePath(@TempDir Path tempDir) throws Exception + { + URI base = URI.create("http://localhost/"); + URI uri = URI.create("http://localhost/myfile"); + URI uploadRoot = tempDir.toUri(); // ends with '/' + byte[] data = "content-addressed".getBytes(StandardCharsets.UTF_8); + + File written = gs.writeFile(uri, base, uploadRoot, new ByteArrayInputStream(data)); + + assertEquals(new File(uploadRoot.resolve("myfile")), written); + assertArrayEquals(data, Files.readAllBytes(written.toPath())); + } + + // writeFiles(Model, Map) + + @Test + public void testWriteFilesNullModel() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFiles(null, new HashMap<>())); + } + + @Test + public void testWriteFilesNullMap() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFiles(ModelFactory.createDefaultModel(), null)); + } + + @Test + public void testWriteFilesNoFileResourcesWritesNothing() + { + Model model = ModelFactory.createDefaultModel(); + model.createResource("http://localhost/doc").addProperty(DCTerms.creator, model.createResource("http://localhost/agent")); + + assertEquals(0, gs.writeFiles(model, new HashMap<>())); + } + + @Test + public void testChangedResourcesUnchangedDoesNotContainSubject() + { + Model before = ModelFactory.createDefaultModel(); + Resource subject = before.createResource("http://localhost/doc"); + subject.addProperty(DCTerms.creator, before.createResource("http://localhost/agent")); + Model after = ModelFactory.createDefaultModel().add(before); + + assertFalse(gs.getChangedResources(before, after).contains(subject)); + } + +} From 997da5233a6e19c416c73e40c676267407151016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 21 Jun 2026 11:30:33 +0200 Subject: [PATCH 13/30] Jena 6 ont-api (#316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate vocabularies to Jena ontapi; add ontology characterization tests - Bump client dependency to 4.3.1-SNAPSHOT (pulls jena-ontapi transitively via Core) - Migrate 19 vocabularies off the deprecated-for-removal org.apache.jena.ontology API to org.apache.jena.ontapi (OntModelFactory/OntSpecification), with a top-of-class static JenaSystem.init() guard (ontapi NPEs if a vocab class is the first Jena touch) - Add characterization tests pinning current ontology behavior so the migration can be proven to retain it: imports closure + RDFS materialization, dual-key cache (addDocumentModel), SPARQL-first resolution (OntologyModelGetter), vocab triples Co-Authored-By: Claude Opus 4.8 (1M context) * Phase D: dissolve DataManager + migrate ontology pipeline to ontapi Completes the dissolution in LinkedDataHub (0 [removal] warnings, was ~90): - DataManagerImpl -> SameSiteSourceResolver (RDFSourceResolver subclass, same-site restriction) - OntologyModelGetter -> OntologyRepository (SPARQL-first PrefixGraphRepository subclass) - DataManagerFactory -> SourceResolverFactory (provides request-scoped RDFSourceResolver) - Application: per-app OntModelSpec/OntDocumentManager -> per-app OntologyRepository; global repository + resolver; remove PrefixMapper/LocationMapper/OntDocumentManager wiring - OntologyFilter/ClearOntology: ontapi OntModelFactory.createModel with OWL2_DL_MEM_RDFS_INF + materialize into OWL2_DL_MEM cached in the repository; dual-key import-closure caching preserved - request ontology property Optional -> Optional (ontapi) across OntologyFactory + ~8 consumers (.getOntModel() dropped) - Validator, ProxyRequestFilter, ValidatingModelProvider, Install/UninstallPackage: DataManager/OntModelSpec cache ops -> repository - characterization tests adapted to new API (OntologyRepositoryTest, OntologyFilterTest); 69 tests green Co-Authored-By: Claude Opus 4.8 (1M context) * Fix OntologyFilter: flatten owl:imports closure manually, not via ontapi union-graph repository OntModelFactory.createModel(graph, spec, repository) registers graphs in an OntUnionGraphRepository keyed by ontology ID, which collided with the same graph already in our flat PrefixGraphRepository cache (OntJenaException: 'Another graph with name <...#> is already in the hierarchy', thrown at runtime on the first /ns request). Replace it with explicit owl:imports closure traversal (loadClosure) that unions the base + imports into one graph, runs RDFS inference over that, and materializes — so createModel is only ever called WITHOUT a repository (no union-hierarchy registration). Encapsulates the duplicated OntologyFilter/ClearOntology load logic into OntologyFilter.loadOntology. OntologyImportsCharacterizationTest now exercises loadOntology and asserts the materialized closure. Co-Authored-By: Claude Opus 4.8 (1M context) * Fix empty constructed response: use OWL1_FULL_MEM so rdfs:Class is recognized GET /ns?forClass=...#Item returned an empty graph: dh:Item is declared 'a rdfs:Class' (dh.ttl), and the ontapi OWL2_DL_MEM profile I'd used does not recognize rdfs:Class as an OntClass, so Namespace's getOntClass(uri) returned null and no SPIN constructor ran. Legacy OntModelSpec.OWL_MEM is OWL 1 Full (recognizes rdfs:Class); the correct ontapi mapping is OWL1_FULL_MEM, not OWL2_DL_MEM. Applied across OntologyFilter pipeline + all vocab holders. OntologyImportsCharacterizationTest now guards getOntClass(rdfs:Class) != null. Co-Authored-By: Claude Opus 4.8 (1M context) * Use twirl 1.2.0-SNAPSHOT; drop redundant SPIN personality registration twirl 1.2.0 re-bases the constraint model onto its own SPIN personality, so the ontapi ontology model can be passed straight to SPINConstraints.check; LDH no longer needs to register SPIN into the global personality. - twirl 1.1.0 -> 1.2.0-SNAPSHOT - Remove SP.init(BuiltinPersonalities.model) from Application (Web-Client Constructor reads sp:text directly; nothing else needed the global registration) - Add SPINConstraintValidationTest: guards the Validator path and the raw-ontapi-via-twirl-rebase path Co-Authored-By: Claude Opus 4.8 (1M context) * ci: re-run against deployed twirl/core snapshots * ci: re-run against deployed twirl/core snapshots * Normalize ontologies to owl:Class; use OWL2_FULL_MEM profile The document/LDT/ACL ontology classes were declared bare rdfs:Class, which no OWL2 ontapi profile recognizes as an OntClass — forcing OWL1_FULL_MEM (which in turn bans named individuals, breaking SD). Declare them owl:Class (dual with rdfs:Class) and switch all OntSpecification usages from OWL1_FULL_MEM to OWL2_FULL_MEM, so getOntClass/forClass works and named individuals are allowed. - dh.ttl/ldt.ttl/lacl.ttl: class declarations now 'a rdfs:Class, owl:Class' - vocab holders + OntologyFilter + ConstructForClass: OWL1_FULL_MEM -> OWL2_FULL_MEM - remove dead 'import com.atomgraph.client.locator.PrefixMapper' (deleted in Web-Client) - OntologyImportsCharacterizationTest: assert owl:Class (not rdfs:Class) is recognized Co-Authored-By: Claude Opus 4.8 (1M context) * Add debug output to add-property-constraint and GET-proxied-ontology-ns tests Co-Authored-By: Claude Opus 4.8 (1M context) * Removed debug output * Tests for RDFS ontology imports * Promote rdfs:Class to owl:Class during ontology materialization OWL2 profiles do not recognise bare rdfs:Class as OntClass, so third-party vocab terms declared only as rdfs:Class (e.g. sp:Describe in sp.ttl) were invisible to getOntClass() lookups, returning empty forClass constructor responses. Co-Authored-By: Claude Opus 4.8 (1M context) * Repoint PrefixGraphRepository import to Web-Client; replace deprecated Model.write with RDFWriter - Update imports to com.atomgraph.client.util.jena.PrefixGraphRepository (moved out of Core): Application, OntologyFilter, OntologyRepository, SourceResolverFactory, SameSiteSourceResolver + tests - XSLTWriterBase: use RDFWriter instead of deprecated Model.write, keeping plain RDF/XML Co-Authored-By: Claude Opus 4.8 (1M context) * SNAPSHOT bump --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../GET-namespace-forClass-rdfs.sh | 22 ++ pom.xml | 8 +- .../atomgraph/linkeddatahub/Application.java | 155 ++++++------- .../linkeddatahub/resource/Generate.java | 10 +- .../linkeddatahub/resource/Namespace.java | 18 +- .../resource/admin/ClearOntology.java | 26 +-- .../linkeddatahub/resource/admin/SignUp.java | 4 +- .../resource/admin/pkg/InstallPackage.java | 34 +-- .../resource/admin/pkg/UninstallPackage.java | 25 +-- .../server/event/AuthorizationCreated.java | 1 - .../server/factory/OntologyFactory.java | 12 +- .../server/filter/request/OntologyFilter.java | 154 ++++++------- .../filter/request/ProxyRequestFilter.java | 12 +- .../server/io/ValidatingModelProvider.java | 2 +- .../impl/DocumentHierarchyGraphStoreImpl.java | 8 +- ...delGetter.java => OntologyRepository.java} | 74 +++---- .../linkeddatahub/vocabulary/ACL.java | 45 ++-- .../linkeddatahub/vocabulary/Admin.java | 14 +- .../linkeddatahub/vocabulary/Cert.java | 28 ++- .../linkeddatahub/vocabulary/DH.java | 26 ++- .../linkeddatahub/vocabulary/Default.java | 17 +- .../linkeddatahub/vocabulary/FOAF.java | 46 ++-- .../linkeddatahub/vocabulary/Google.java | 20 +- .../linkeddatahub/vocabulary/LACL.java | 38 ++-- .../linkeddatahub/vocabulary/LAPP.java | 46 ++-- .../linkeddatahub/vocabulary/LDH.java | 56 ++--- .../linkeddatahub/vocabulary/LDHC.java | 107 ++++----- .../linkeddatahub/vocabulary/LDHT.java | 21 +- .../linkeddatahub/vocabulary/NFO.java | 21 +- .../linkeddatahub/vocabulary/ORCID.java | 20 +- .../linkeddatahub/vocabulary/PROV.java | 38 ++-- .../linkeddatahub/vocabulary/SIOC.java | 206 +++++++++--------- .../linkeddatahub/vocabulary/VoID.java | 28 ++- .../linkeddatahub/writer/ModelXSLTWriter.java | 7 +- .../writer/ResultSetXSLTWriter.java | 7 +- .../linkeddatahub/writer/XSLTWriterBase.java | 46 ++-- ...actory.java => SourceResolverFactory.java} | 47 ++-- ...rImpl.java => SameSiteSourceResolver.java} | 72 ++---- .../server/io/ValidatingDatasetProvider.java | 10 +- .../server/io/ValidatingModelProvider.java | 10 +- .../com/atomgraph/server/util/Validator.java | 2 +- .../com/atomgraph/server/vocabulary/HTTP.java | 30 +-- .../com/atomgraph/server/vocabulary/LDT.java | 64 +++--- .../linkeddatahub/app/admin/lacl.ttl | 4 +- .../com/atomgraph/linkeddatahub/dh.ttl | 6 +- .../com/atomgraph/linkeddatahub/ldt.ttl | 14 +- .../filter/request/OntologyFilterTest.java | 68 ++++++ .../OntologyImportsCharacterizationTest.java | 91 ++++++++ .../server/util/OntologyRepositoryTest.java | 111 ++++++++++ .../util/SPINConstraintValidationTest.java | 98 +++++++++ .../vocabulary/VocabularyHolderTest.java | 63 ++++++ 51 files changed, 1264 insertions(+), 828 deletions(-) create mode 100644 http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh rename src/main/java/com/atomgraph/linkeddatahub/server/util/{OntologyModelGetter.java => OntologyRepository.java} (64%) rename src/main/java/com/atomgraph/linkeddatahub/writer/factory/{DataManagerFactory.java => SourceResolverFactory.java} (70%) rename src/main/java/com/atomgraph/linkeddatahub/writer/impl/{DataManagerImpl.java => SameSiteSourceResolver.java} (59%) create mode 100644 src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java create mode 100644 src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java create mode 100644 src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java create mode 100644 src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java create mode 100644 src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java diff --git a/http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh b/http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh new file mode 100644 index 000000000..e363dfbcf --- /dev/null +++ b/http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh @@ -0,0 +1,22 @@ +#!/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" + +# sp:Describe is declared only as rdfs:Class (not owl:Class) in sp.ttl. +# OntologyFilter must promote rdfs:Class to owl:Class during materialization so +# that OWL2 profiles recognise third-party vocab terms and return their SPIN constructors. + +response=$(curl -k -f -s \ + -G \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: application/rdf+xml" \ + --data-urlencode "forClass=http://spinrdf.org/sp#Describe" \ + "${END_USER_BASE_URL}ns") + +# response must be non-empty: sp:Describe must be recognised as an OntClass +echo "$response" | grep -q "http://spinrdf.org/sp#Describe" diff --git a/pom.xml b/pom.xml index bcdd7851c..f8fb12086 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.5.4-SNAPSHOT + 5.6.0-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub @@ -146,7 +146,7 @@ ${project.groupId} twirl - 1.1.0 + 1.2.0-SNAPSHOT @@ -163,13 +163,13 @@ ${project.groupId} client - 4.3.0 + 4.4.0-SNAPSHOT classes ${project.groupId} client - 4.3.0 + 4.4.0-SNAPSHOT war diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index abe64f03d..d10215d95 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -16,6 +16,14 @@ */ package com.atomgraph.linkeddatahub; +import com.atomgraph.client.util.jena.PrefixGraphRepository; +import com.atomgraph.client.util.RDFSourceResolver; +import com.atomgraph.client.util.StylesheetResolver; +import com.atomgraph.linkeddatahub.writer.impl.SameSiteSourceResolver; +import com.atomgraph.linkeddatahub.server.util.OntologyRepository; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; import com.atomgraph.linkeddatahub.server.mapper.HttpHostConnectExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.InternalURLExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.MessagingExceptionMapper; @@ -26,10 +34,6 @@ import com.atomgraph.linkeddatahub.server.mapper.ForbiddenExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.auth.webid.WebIDCertificateExceptionMapper; import com.atomgraph.client.MediaTypes; -import com.atomgraph.client.locator.PrefixMapper; -import org.apache.jena.ontology.OntDocumentManager; -import org.apache.jena.util.FileManager; -import org.apache.jena.util.LocationMapper; import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletConfig; import jakarta.ws.rs.core.Context; @@ -37,8 +41,6 @@ import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFWriterRegistry; import com.atomgraph.client.mapper.ClientErrorExceptionMapper; -import com.atomgraph.client.util.DataManager; -import com.atomgraph.client.util.DataManagerImpl; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.client.writer.function.UUID; import com.atomgraph.core.exception.ConfigurationException; @@ -49,7 +51,7 @@ import com.atomgraph.core.io.UpdateRequestProvider; import com.atomgraph.core.mapper.BadGatewayExceptionMapper; import com.atomgraph.core.provider.QueryParamProvider; -import com.atomgraph.linkeddatahub.writer.factory.DataManagerFactory; +import com.atomgraph.linkeddatahub.writer.factory.SourceResolverFactory; import com.atomgraph.server.vocabulary.LDT; import com.atomgraph.server.mapper.NotFoundExceptionMapper; import com.atomgraph.core.riot.RDFLanguages; @@ -67,7 +69,6 @@ import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.writer.factory.xslt.XsltExecutableSupplier; import com.atomgraph.linkeddatahub.writer.factory.XsltExecutableSupplierFactory; -import com.atomgraph.client.util.XsltResolver; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.client.filter.ClientUriRewriteFilter; import com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilter; @@ -135,7 +136,6 @@ import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import org.apache.jena.enhanced.BuiltinPersonalities; -import org.apache.jena.ontology.OntModelSpec; import org.apache.jena.riot.RDFParserRegistry; import org.slf4j.Logger; import java.net.URI; @@ -164,7 +164,7 @@ import javax.net.ssl.TrustManagerFactory; import jakarta.servlet.ServletContext; import javax.xml.transform.Source; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.Dataset; import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; @@ -174,7 +174,6 @@ import org.slf4j.LoggerFactory; import com.atomgraph.server.mapper.SHACLConstraintViolationExceptionMapper; import com.atomgraph.server.mapper.SPINConstraintViolationExceptionMapper; -import com.atomgraph.spinrdf.vocabulary.SP; import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.JsonLdOptions; import java.io.FileOutputStream; @@ -268,8 +267,9 @@ public class Application extends ResourceConfig private final ExecutorService importThreadPool; private final ServletConfig servletConfig; private final EventBus eventBus = new EventBus(); - private final DataManager dataManager; - private final Map endUserOntModelSpecs; + private final PrefixGraphRepository repository; + private final SameSiteSourceResolver resolver; + private final Map endUserRepositories; private final MediaTypes mediaTypes; private final Client client, externalClient, importClient, noCertClient; private final Query documentTypeQuery, documentOwnerQuery, aclQuery, ownerAclQuery, webIDQuery, agentQuery, userAccountQuery, ontologyQuery; // no relative URIs @@ -278,7 +278,6 @@ public class Application extends ResourceConfig private final Processor xsltProc = new Processor(false); private final XsltCompiler xsltComp; private final XsltExecutable xsltExec; - private final OntModelSpec ontModelSpec; private final boolean cacheStylesheet; private final boolean resolvingUncached; private final URI baseURI, uploadRoot; @@ -324,7 +323,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI())) : null, servletConfig.getServletContext().getInitParameter(A.cacheModelLoads.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.cacheModelLoads.getURI())) : true, servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI())) : false, - new PrefixMapper(servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()) != null ? servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()) : null), + servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()), servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) != null ? servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) : null, com.atomgraph.client.Application.getSource(servletConfig.getServletContext(), servletConfig.getServletContext().getInitParameter(AC.stylesheet.getURI()) != null ? servletConfig.getServletContext().getInitParameter(AC.stylesheet.getURI()) : null), servletConfig.getServletContext().getInitParameter(AC.cacheStylesheet.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(AC.cacheStylesheet.getURI())) : false, @@ -435,7 +434,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti */ public Application(final ServletConfig servletConfig, final MediaTypes mediaTypes, final Integer maxGetRequestSize, final boolean cacheModelLoads, final boolean preemptiveAuth, - final LocationMapper locationMapper, final String contextDatasetURIString, + final String prefixMappingConfig, final String contextDatasetURIString, final Source stylesheet, final boolean cacheStylesheet, final boolean resolvingUncached, final String clientKeyStoreURIString, final String clientKeyStorePassword, final String secretaryCertAlias, @@ -682,12 +681,8 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType } // register plain RDF/XML writer as default - RDFWriterRegistry.register(Lang.RDFXML, RDFFormat.RDFXML_PLAIN); + RDFWriterRegistry.register(Lang.RDFXML, RDFFormat.RDFXML_PLAIN); - // initialize mapping for locally stored vocabularies - LocationMapper.setGlobalLocationMapper(locationMapper); - if (log.isTraceEnabled()) log.trace("LocationMapper.get(): {}", locationMapper); - try { this.contextDataset = getDataset(servletConfig.getServletContext(), contextDatasetURI); @@ -747,7 +742,6 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType throw new IllegalStateException(new CertificateException("Secretary certificate with alias " + secretaryCertAlias + " not a valid WebID sertificate (SNA URI is missing)")); } - SP.init(BuiltinPersonalities.model); BuiltinPersonalities.model.add(Authorization.class, AuthorizationImpl.factory); BuiltinPersonalities.model.add(Agent.class, AgentImpl.factory); BuiltinPersonalities.model.add(UserAccount.class, UserAccountImpl.factory); @@ -792,14 +786,16 @@ else if (app.hasProperty(RDF.type, LAPP.EndUserApplication)) serviceIt.close(); } - // TO-DO: config property for cacheModelLoads - endUserOntModelSpecs = new HashMap<>(); - dataManager = new DataManagerImpl(locationMapper, new HashMap<>(), GraphStoreClient.create(client, mediaTypes), cacheModelLoads, preemptiveAuth, resolvingUncached); - ontModelSpec = OntModelSpec.OWL_MEM_RDFS_INF; - ontModelSpec.setImportModelGetter(dataManager); - OntDocumentManager.getInstance().setFileManager((FileManager)dataManager); - OntDocumentManager.getInstance().setCacheModels(true); // need to re-set after changing FileManager - ontModelSpec.setDocumentManager(OntDocumentManager.getInstance()); + endUserRepositories = new HashMap<>(); + // global graph repository: bundled vocabularies/ontologies mapped from the prefix-mapping config + repository = new PrefixGraphRepository(GraphStoreClient.create(client, mediaTypes)); + if (prefixMappingConfig != null) + { + Model prefixMappingModel = ModelFactory.createDefaultModel(); + RDFParser.create().source(prefixMappingConfig).streamManager(repository.getStreamManager()).build().parse(prefixMappingModel); + repository.processConfig(prefixMappingModel); + } + resolver = new SameSiteSourceResolver(repository, GraphStoreClient.create(client, mediaTypes), resolvingUncached, baseURI); if (mailUser != null && mailPassword != null) // enable SMTP authentication { @@ -836,19 +832,15 @@ protected PasswordAuthentication getPasswordAuthentication() xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.URLDecode()); xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.SendHTTPRequest(xsltProc, client)); - Model mappingModel = locationMapper.toModel(); - ResIterator prefixedMappings = mappingModel.listResourcesWithProperty(LocationMappingVocab.prefix); try { - while (prefixedMappings.hasNext()) + for (String prefix : getRepository().getPrefixMappings().keySet()) { - Resource prefixMapping = prefixedMappings.next(); - String prefix = prefixMapping.getRequiredProperty(LocationMappingVocab.prefix).getString(); // register mapped RDF documents in the XSLT processor so that document() returns them cached, throughout multiple transformations - TreeInfo doc = xsltProc.getUnderlyingConfiguration().buildDocumentTree(dataManager.resolve("", prefix)); + TreeInfo doc = xsltProc.getUnderlyingConfiguration().buildDocumentTree(getResolver().resolve("", prefix)); xsltProc.getUnderlyingConfiguration().getGlobalDocumentPool().add(doc, prefix); } - + // register HTTPS URL of translations.rdf so it doesn't have to be requested repeatedly try (InputStream translations = servletConfig.getServletContext().getResourceAsStream(XSLTWriterBase.TRANSLATIONS_PATH)) { @@ -861,14 +853,10 @@ protected PasswordAuthentication getPasswordAuthentication() if (log.isErrorEnabled()) log.error("Error reading mapped RDF document: {}", ex); throw new IllegalStateException(ex); } - finally - { - prefixedMappings.close(); - } xsltComp = xsltProc.newXsltCompiler(); xsltComp.setParameter(new QName("ldh", LDH.base.getNameSpace(), LDH.base.getLocalName()), new XdmAtomicValue(baseURI)); - xsltComp.setURIResolver(new XsltResolver(LocationMapper.get(), new HashMap<>(), GraphStoreClient.create(client, mediaTypes), false, false, true)); // default Xerces parser does not support HTTPS + xsltComp.setURIResolver(new StylesheetResolver(getRepository(), GraphStoreClient.create(client, mediaTypes))); // resolves xsl:import to raw stylesheet sources xsltExec = xsltComp.compile(stylesheet); } catch (FileNotFoundException ex) @@ -935,8 +923,8 @@ public void init() register(new QueryProvider()); register(new QueryParamProvider()); register(new UpdateRequestProvider()); - register(new ModelXSLTWriter(getXsltExecutable(), getOntModelSpec(), getDataManager(), getMessageDigest())); // writes (X)HTML responses - register(new ResultSetXSLTWriter(getXsltExecutable(), getOntModelSpec(), getDataManager(), getMessageDigest())); // writes (X)HTML responses + register(new ModelXSLTWriter(getXsltExecutable(), getResolver(), getMessageDigest())); // writes (X)HTML responses + register(new ResultSetXSLTWriter(getXsltExecutable(), getResolver(), getMessageDigest())); // writes (X)HTML responses final com.atomgraph.linkeddatahub.Application system = this; register(new AbstractBinder() @@ -1014,7 +1002,7 @@ protected void configure() @Override protected void configure() { - bindFactory(OntologyFactory.class).to(new TypeLiteral>() {}). + bindFactory(OntologyFactory.class).to(new TypeLiteral>() {}). in(RequestScoped.class); } }); @@ -1023,15 +1011,7 @@ protected void configure() @Override protected void configure() { - bindFactory(new com.atomgraph.core.factory.DataManagerFactory(getDataManager())).to(com.atomgraph.core.util.jena.DataManager.class); - } - }); - register(new AbstractBinder() - { - @Override - protected void configure() - { - bindFactory(DataManagerFactory.class).to(com.atomgraph.client.util.DataManager.class). + bindFactory(SourceResolverFactory.class).to(com.atomgraph.client.util.RDFSourceResolver.class). in(RequestScoped.class); } }); @@ -1756,43 +1736,54 @@ public EventBus getEventBus() } /** - * Gets Jena's DataManager implementation. - * - * @return data manager instance + * Returns the global graph repository (bundled vocabularies/ontologies + URI mapping + cache). + * + * @return graph repository + */ + public PrefixGraphRepository getRepository() + { + return repository; + } + + /** + * Returns the global XSLT source resolver. + * + * @return source resolver */ - public DataManager getDataManager() + public SameSiteSourceResolver getResolver() { - return dataManager; + return resolver; } - + /** - * Returns a map of application URIs to ontology specifications. - * - * @return URI to ontology specification map + * Returns a map of application URIs to ontology repositories. + * + * @return URI to ontology repository map */ - protected Map getEndUserOntModelSpecs() + protected Map getEndUserRepositories() { - return endUserOntModelSpecs; + return endUserRepositories; } /** - * Returns ontology specification for the specified end-user application. - * + * Returns the SPARQL-first ontology repository for the specified end-user application. + * * @param app end-user application resource - * @return ontology specification + * @return ontology repository */ - public OntModelSpec getOntModelSpec(EndUserApplication app) + public OntologyRepository getRepository(EndUserApplication app) { - if (!getEndUserOntModelSpecs().containsKey(app.getURI())) + if (!getEndUserRepositories().containsKey(app.getURI())) { - OntModelSpec appOntModelSpec = new OntModelSpec(OntModelSpec.OWL_MEM_RDFS_INF); - appOntModelSpec.setDocumentManager(new OntDocumentManager()); - appOntModelSpec.getDocumentManager().setFileManager(new DataManagerImpl(LocationMapper.get(), new HashMap<>(), GraphStoreClient.create(getClient(), getMediaTypes()), true, isPreemptiveAuth(), isResolvingUncached())); - - getEndUserOntModelSpecs().put(app.getURI(), appOntModelSpec); + OntologyRepository appRepository = new OntologyRepository(app, this, GraphStoreClient.create(getClient(), getMediaTypes()), getOntologyQuery()); + // seed bundled vocabulary/ontology mappings from the global repository + getRepository().getLocationMappings().forEach(appRepository::addLocationMapping); + getRepository().getPrefixMappings().forEach(appRepository::addPrefixMapping); + + getEndUserRepositories().put(app.getURI(), appRepository); } - - return getEndUserOntModelSpecs().get(app.getURI()); + + return getEndUserRepositories().get(app.getURI()); } /** @@ -2002,16 +1993,6 @@ public boolean isPreemptiveAuth() return preemptiveAuth; } - /** - * The default specification of ontology models. - * - * @return spec object - */ - public OntModelSpec getOntModelSpec() - { - return ontModelSpec; - } - /** * Returns Saxon's XSLT compiler. * diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java index 34b68d81f..27e2124ff 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java @@ -42,7 +42,7 @@ import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.ontology.Ontology; +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; @@ -69,7 +69,7 @@ public class Generate private final UriInfo uriInfo; private final MediaTypes mediaTypes; private final Application application; - private final Ontology ontology; + private final OntModel ontology; private final Optional agentContext; private final com.atomgraph.linkeddatahub.Application system; private final ResourceContext resourceContext; @@ -88,7 +88,7 @@ public class Generate */ @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.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"); @@ -134,7 +134,7 @@ public Response post(Model model) if (queryRes == null) throw new BadRequestException("Container query string (spin:query) not provided"); // Lookup query in ontology - Resource queryResource = getOntology().getOntModel().getResource(queryRes.getURI()); + Resource queryResource = getOntology().getResource(queryRes.getURI()); if (queryResource == null || !queryResource.hasProperty(SP.text)) throw new BadRequestException("Query resource not found in ontology: " + queryRes.getURI()); @@ -265,7 +265,7 @@ public Application getApplication() * * @return the ontology */ - public Ontology getOntology() + public OntModel getOntology() { return ontology; } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java index 4ccefa319..c055f957d 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java @@ -31,7 +31,7 @@ import com.atomgraph.core.model.impl.dataset.ServiceImpl; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter; +import com.atomgraph.linkeddatahub.server.util.OntologyRepository; import java.net.URI; import java.util.List; import java.util.Optional; @@ -47,7 +47,7 @@ import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; import org.apache.jena.irix.IRIx; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.Query; import org.apache.jena.query.QueryFactory; @@ -70,7 +70,7 @@ public class Namespace extends com.atomgraph.core.model.impl.SPARQLEndpointImpl private final URI uri; private final UriInfo uriInfo; private final Application application; - private final Ontology ontology; + private final OntModel ontology; private final com.atomgraph.linkeddatahub.Application system; /** @@ -86,10 +86,10 @@ public class Namespace extends com.atomgraph.core.model.impl.SPARQLEndpointImpl */ @Inject public Namespace(@Context Request request, @Context UriInfo uriInfo, - Application application, Optional ontology, MediaTypes mediaTypes, + Application application, Optional ontology, MediaTypes mediaTypes, @Context SecurityContext securityContext, com.atomgraph.linkeddatahub.Application system) { - super(request, new ServiceImpl(DatasetFactory.create(ontology.get().getOntModel()), mediaTypes), mediaTypes); + super(request, new ServiceImpl(DatasetFactory.create(ontology.get()), mediaTypes), mediaTypes); this.uri = uriInfo.getAbsolutePath(); this.uriInfo = uriInfo; this.application = application; @@ -128,7 +128,7 @@ public Response get(@QueryParam(QUERY) Query query, Model instances = ModelFactory.createDefaultModel(); forClasses.stream(). - map(forClass -> Optional.ofNullable(getOntology().getOntModel().getOntClass(checkURI(forClass).toString()))). + map(forClass -> Optional.ofNullable(getOntology().getOntClass(checkURI(forClass).toString()))). flatMap(Optional::stream). forEach(forClass -> new Constructor().construct(forClass, instances, getApplication().getBase().getURI())); @@ -141,8 +141,8 @@ public Response get(@QueryParam(QUERY) Query query, String ontologyURI = getApplication().getOntology().getURI(); if (log.isDebugEnabled()) log.debug("Returning namespace ontology from OntDocumentManager: {}", ontologyURI); // not returning the injected in-memory ontology because it has inferences applied to it - OntologyModelGetter modelGetter = new OntologyModelGetter(getApplication().as(EndUserApplication.class), getSystem(), getSystem().getOntModelSpec(), getSystem().getOntologyQuery()); - return getResponseBuilder(modelGetter.getModel(ontologyURI)).build(); + OntologyRepository repository = new OntologyRepository(getApplication().as(EndUserApplication.class), getSystem(), com.atomgraph.linkeddatahub.client.GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()), getSystem().getOntologyQuery()); + return getResponseBuilder(org.apache.jena.rdf.model.ModelFactory.createModelForGraph(repository.get(ontologyURI))).build(); } else throw new BadRequestException("SPARQL query string not provided"); } @@ -223,7 +223,7 @@ public Application getApplication() * * @return application ontology */ - public Ontology getOntology() + public OntModel getOntology() { return ontology; } 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 202f72429..75ab778a9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java @@ -18,9 +18,8 @@ import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import static com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter.addDocumentModel; import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter; -import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter; +import com.atomgraph.linkeddatahub.server.util.OntologyRepository; import java.net.URI; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; @@ -31,8 +30,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; +import com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; @@ -82,11 +80,11 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer if (ontologyURI == null) throw new BadRequestException("Ontology URI not specified"); EndUserApplication endUserApp = getApplication().as(AdminApplication.class).getEndUserApplication(); // we're assuming the current app is admin - OntModelSpec ontModelSpec = new OntModelSpec(getSystem().getOntModelSpec(endUserApp)); - if (ontModelSpec.getDocumentManager().getFileManager().hasCachedModel(ontologyURI)) + OntologyRepository repository = getSystem().getRepository(endUserApp); + if (repository.isCached(ontologyURI)) { if (log.isDebugEnabled()) log.debug("Clearing ontology with URI '{}' from memory", ontologyURI); - ontModelSpec.getDocumentManager().getFileManager().removeCacheModel(ontologyURI); + repository.remove(ontologyURI); URI ontologyDocURI = UriBuilder.fromUri(ontologyURI).fragment(null).build(); // skip fragment from the ontology URI to get its graph URI // frontend proxy still uses URL-pattern BAN for direct document GETs (until Stage 3 brings xkey tagging to varnish-frontend). @@ -116,19 +114,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 !!! - // same logic as in OntologyFilter. TO-DO: encapsulate? - OntologyModelGetter modelGetter = new OntologyModelGetter(endUserApp, getSystem(), ontModelSpec, getSystem().getOntologyQuery()); - ontModelSpec.setImportModelGetter(modelGetter); - if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}' from the admin dataset", ontologyURI); - Model baseModel = modelGetter.getModel(ontologyURI); - OntModel ontModel = ModelFactory.createOntologyModel(ontModelSpec, baseModel); - // materialize OntModel inferences to avoid invoking rules engine on every request - OntModel materializedModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); // no inference - materializedModel.add(ontModel); - ontModel.getDocumentManager().addModel(ontologyURI, materializedModel, true); // make immutable and add as OntModel so that imports do not need to be reloaded during retrieval - // make sure to cache imported models not only by ontology URI but also by document URI - ontModel.listImportedOntologyURIs(true).forEach((String importURI) -> addDocumentModel(ontModel.getDocumentManager(), importURI)); - if (log.isDebugEnabled()) log.debug("Finished loading ontology with URI '{}' from the admin dataset", ontologyURI); + OntologyFilter.loadOntology(repository, ontologyURI); } if (referer != null) return Response.seeOther(referer).build(); diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java index d71770a2c..bc8467d0d 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java @@ -72,7 +72,7 @@ import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.ext.Providers; import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDhexBinary; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; import org.apache.jena.query.ResultSet; @@ -144,7 +144,7 @@ public class SignUp extends DocumentHierarchyGraphStoreImpl // TO-DO: move to AuthenticationExceptionMapper and handle as state instead of URI resource? @Inject public SignUp(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, + com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, @Context SecurityContext securityContext, Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system, @Context ServletConfig servletConfig) { diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java index 9e760cd0b..e178f6dc6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java @@ -17,7 +17,6 @@ package com.atomgraph.linkeddatahub.resource.admin.pkg; import static com.atomgraph.client.MediaType.TEXT_XSL; -import com.atomgraph.client.util.DataManager; import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; @@ -60,7 +59,6 @@ import org.apache.jena.ontology.ConversionException; import org.apache.jena.update.UpdateFactory; import org.apache.jena.update.UpdateRequest; -import org.apache.jena.util.FileManager; import com.atomgraph.linkeddatahub.vocabulary.DH; import com.atomgraph.linkeddatahub.vocabulary.FOAF; import com.atomgraph.linkeddatahub.vocabulary.SIOC; @@ -88,7 +86,6 @@ public class InstallPackage private final com.atomgraph.linkeddatahub.apps.model.Application application; private final com.atomgraph.linkeddatahub.Application system; - private final DataManager dataManager; private final Optional agentContext; @Context ServletContext servletContext; @@ -105,12 +102,10 @@ public class InstallPackage @Inject public InstallPackage(com.atomgraph.linkeddatahub.apps.model.Application application, com.atomgraph.linkeddatahub.Application system, - DataManager dataManager, Optional agentContext) { this.application = application; this.system = system; - this.dataManager = dataManager; this.agentContext = agentContext; } @@ -242,12 +237,12 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package final Model model; // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request - if (((FileManager)getDataManager()).hasCachedModel(packageURI) || - (getDataManager().isResolvingMapped() && getDataManager().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file + if (getSystem().getRepository().isCached(packageURI) || + (getSystem().getRepository().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file { - if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, ((FileManager)getDataManager()).hasCachedModel(packageURI)); - if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getDataManager().isMapped(packageURI)); - model = getDataManager().loadModel(packageURI); + if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, getSystem().getRepository().isCached(packageURI)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getSystem().getRepository().isMapped(packageURI)); + model = ModelFactory.createModelForGraph(getSystem().getRepository().get(packageURI)); } else { @@ -284,12 +279,12 @@ private Model downloadOntology(String uri) if (log.isDebugEnabled()) log.debug("Downloading ontology from: {}", uri); // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request - if (((FileManager)getDataManager()).hasCachedModel(uri) || - (getDataManager().isResolvingMapped() && getDataManager().isMapped(uri))) // read mapped URIs (such as system ontologies) from a file + if (getSystem().getRepository().isCached(uri) || + (getSystem().getRepository().isMapped(uri))) // read mapped URIs (such as system ontologies) from a file { - if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", uri, ((FileManager)getDataManager()).hasCachedModel(uri)); - if (log.isDebugEnabled()) log.debug("isMapped({}): {}", uri, getDataManager().isMapped(uri)); - return getDataManager().loadModel(uri); + if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", uri, getSystem().getRepository().isCached(uri)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", uri, getSystem().getRepository().isMapped(uri)); + return ModelFactory.createModelForGraph(getSystem().getRepository().get(uri)); } else { @@ -522,15 +517,6 @@ public ServletContext getServletContext() return servletContext; } - /** - * Returns RDF data manager. - * - * @return RDF data manager - */ - public DataManager getDataManager() - { - return dataManager; - } /** * Returns JAX-RS resource context. diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java index fb0cb5f3c..28d37ca9f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java @@ -16,7 +16,6 @@ */ package com.atomgraph.linkeddatahub.resource.admin.pkg; -import com.atomgraph.client.util.DataManager; import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; @@ -39,10 +38,10 @@ import jakarta.ws.rs.core.UriBuilder; import org.apache.commons.codec.binary.Hex; import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; import org.apache.jena.update.UpdateFactory; import org.apache.jena.update.UpdateRequest; -import org.apache.jena.util.FileManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; @@ -73,7 +72,6 @@ public class UninstallPackage private final com.atomgraph.linkeddatahub.apps.model.Application application; private final com.atomgraph.linkeddatahub.Application system; - private final DataManager dataManager; private final Optional agentContext; @Context ServletContext servletContext; @@ -90,12 +88,10 @@ public class UninstallPackage @Inject public UninstallPackage(com.atomgraph.linkeddatahub.apps.model.Application application, com.atomgraph.linkeddatahub.Application system, - DataManager dataManager, Optional agentContext) { this.application = application; this.system = system; - this.dataManager = dataManager; this.agentContext = agentContext; } @@ -317,12 +313,12 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package final Model model; // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request - if (((FileManager)getDataManager()).hasCachedModel(packageURI) || - (getDataManager().isResolvingMapped() && getDataManager().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file + if (getSystem().getRepository().isCached(packageURI) || + (getSystem().getRepository().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file { - if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, ((FileManager)getDataManager()).hasCachedModel(packageURI)); - if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getDataManager().isMapped(packageURI)); - model = getDataManager().loadModel(packageURI); + if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, getSystem().getRepository().isCached(packageURI)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getSystem().getRepository().isMapped(packageURI)); + model = ModelFactory.createModelForGraph(getSystem().getRepository().get(packageURI)); } else { @@ -350,15 +346,6 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - /** - * Returns RDF data manager. - * - * @return RDF data manager - */ - public DataManager getDataManager() - { - return dataManager; - } /** * Returns JAX-RS resource context. diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java b/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java index 7353e0297..3b02124d2 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java @@ -16,7 +16,6 @@ */ package com.atomgraph.linkeddatahub.server.event; -import com.atomgraph.core.util.jena.DataManager; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import org.apache.jena.rdf.model.Resource; diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java b/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java index 39fe494b6..8fc56c6a2 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java @@ -20,7 +20,7 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.ext.Provider; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.vocabulary.OWL; import org.glassfish.hk2.api.Factory; import org.glassfish.hk2.api.ServiceLocator; @@ -31,19 +31,19 @@ * @author Martynas Jusevičius {@literal } */ @Provider -public class OntologyFactory implements Factory> +public class OntologyFactory implements Factory> { @Context private ServiceLocator serviceLocator; @Override - public Optional provide() + public Optional provide() { return getOntology(); } @Override - public void dispose(Optional t) + public void dispose(Optional t) { } @@ -52,9 +52,9 @@ public void dispose(Optional t) * * @return ontology */ - public Optional getOntology() + public Optional getOntology() { - return (Optional)getContainerRequestContext().getProperty(OWL.Ontology.getURI()); + return (Optional)getContainerRequestContext().getProperty(OWL.Ontology.getURI()); } /** 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 03ef83662..5312cc2e5 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 @@ -18,27 +18,28 @@ 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.vocabulary.LAPP; import com.atomgraph.server.exception.OntologyException; -import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter; 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.InternalServerErrorException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.PreMatching; -import org.apache.jena.ontology.OntDocumentManager; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.util.FileManager; import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,7 +101,7 @@ public void filter(ContainerRequestContext crc) throws IOException * @param crc request context * @return optional ontology */ - public Optional getOntology(ContainerRequestContext crc) + public Optional getOntology(ContainerRequestContext crc) { Optional appOpt = getApplication(crc); @@ -115,99 +116,104 @@ public Optional getOntology(ContainerRequestContext crc) return Optional.empty(); } } - + /** * Gets ontology of the specified application. - * + * * @param app application resource - * @return ontology resource + * @return ontology model */ - public Ontology getOntology(Application app) + public OntModel getOntology(Application app) { if (app.getOntology() == null) return null; return getOntology(app, app.getOntology().getURI()); } - + /** - * Loads ontology using the specified ontology URI. - * + * 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. + * * @param app application resource * @param uri ontology URI - * @return ontology resource + * @return ontology model */ - public Ontology getOntology(Application app, String uri) + public OntModel getOntology(Application app, String uri) { - if (app == null) throw new IllegalArgumentException("Application string cannot be null"); - if (uri == null) throw new IllegalArgumentException("Ontology URI string cannot be null"); + if (app == null) throw new IllegalArgumentException("Application cannot be null"); + if (uri == null) throw new IllegalArgumentException("Ontology URI cannot be null"); - final OntModelSpec ontModelSpec; - if (app.canAs(EndUserApplication.class)) - { - ontModelSpec = new OntModelSpec(getSystem().getOntModelSpec(app.as(EndUserApplication.class))); - // only create InfModel if ontology is not already cached - if (!ontModelSpec.getDocumentManager().getFileManager().hasCachedModel(uri)) - { - OntologyModelGetter modelGetter = new OntologyModelGetter(app.as(EndUserApplication.class), getSystem(), ontModelSpec, getSystem().getOntologyQuery()); - ontModelSpec.setImportModelGetter(modelGetter); - if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}' from the admin dataset", uri); - Model baseModel = modelGetter.getModel(uri); - OntModel ontModel = ModelFactory.createOntologyModel(ontModelSpec, baseModel); - // materialize OntModel inferences to avoid invoking rules engine on every request - OntModel materializedModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); // no inference - materializedModel.add(ontModel); - ontModel.getDocumentManager().addModel(uri, materializedModel, true); // make immutable and add as OntModel so that imports do not need to be reloaded during retrieval - // make sure to cache imported models not only by ontology URI but also by document URI - ontModel.listImportedOntologyURIs(true).forEach((String importURI) -> addDocumentModel(ontModel.getDocumentManager(), importURI)); - if (log.isDebugEnabled()) log.debug("Finished loading ontology with URI '{}' from the admin dataset", uri); - } - } - else + 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 + if (!repository.isCached(uri)) loadOntology(repository, uri); + + return OntModelFactory.createModel(repository.get(uri), 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. + * + * @param repository graph repository + * @param uri ontology URI + */ + public static void 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)); + 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 -> { - ontModelSpec = new OntModelSpec(getSystem().getOntModelSpec()); - FileManager fileManager = ontModelSpec.getDocumentManager().getFileManager(); - if (!fileManager.hasCachedModel(uri)) - { - try - { - URI ontologyURI = URI.create(uri); - // remove fragment and normalize - URI ontDocURI = new URI(ontologyURI.getScheme(), ontologyURI.getSchemeSpecificPart(), null).normalize(); - Model baseModel = fileManager.loadModel(ontDocURI.toString()); - OntModel ontModel = ModelFactory.createOntologyModel(ontModelSpec, baseModel); - ontModel.getDocumentManager().addModel(uri, ontModel, true); - } - catch (URISyntaxException ex) - { - if (log.isErrorEnabled()) log.error("Ontology URI syntax error: {}", ex.getInput()); - throw new InternalServerErrorException(ex); - } - } - } - return ontModelSpec.getDocumentManager().getOntology(uri, ontModelSpec).getOntology(uri); // reloads the imports using ModelGetter. TO-DO: optimize? + if (imp.isURIResource()) loadClosure(repository, imp.asResource().getURI(), union, seen); + }); } /** - * Extracts document URI from ontology import URI and uses it as a secondary cache key. - * - * @param odm document manager + * Caches an imported graph under its fragment-stripped document URI as a secondary cache key. + * + * @param repository graph repository * @param importURI ontology URI */ - public static void addDocumentModel(OntDocumentManager odm, String importURI) + public static void addDocumentModel(PrefixGraphRepository repository, String importURI) { try { URI ontologyURI = URI.create(importURI); // remove fragment and normalize URI docURI = new URI(ontologyURI.getScheme(), ontologyURI.getSchemeSpecificPart(), null).normalize(); - String mappedURI = odm.getFileManager().mapURI(docURI.toString()); - // only cache import document URI if it's not already cached or mapped - if (!odm.getFileManager().hasCachedModel(docURI.toString()) && mappedURI.equals(docURI.toString())) - { - Model importModel = odm.getModel(importURI); - if (importModel == null) throw new IllegalArgumentException("Import model is not cached"); - odm.addModel(docURI.toString(), importModel, true); - } + // only cache the document URI if it is not already cached or mapped to a different location + if (!repository.isCached(docURI.toString()) && repository.resolve(docURI.toString()).equals(docURI.toString())) + repository.put(docURI.toString(), repository.get(importURI)); } catch (URISyntaxException ex) { 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 4eeb6599e..9166b28ea 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,7 @@ import com.atomgraph.core.exception.BadGatewayException; import com.atomgraph.core.util.ModelUtils; import com.atomgraph.linkeddatahub.apps.model.Dataset; -import org.apache.jena.ontology.Ontology; +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; @@ -133,7 +133,7 @@ public class ProxyRequestFilter implements ContainerRequestFilter "Age"); @Inject com.atomgraph.linkeddatahub.Application system; - @Inject jakarta.inject.Provider> ontology; + @Inject jakarta.inject.Provider> ontology; @Inject MediaTypes mediaTypes; @Context Request request; @@ -178,10 +178,10 @@ public void filter(ContainerRequestContext requestContext) throws IOException || "HEAD".equalsIgnoreCase(requestContext.getMethod()); // serve mapped URIs (e.g. system ontologies) directly from the DataManager cache - if (isSafeMethod && getSystem().getDataManager().isMapped(targetURI.toString())) + if (isSafeMethod && getSystem().getRepository().isMapped(targetURI.toString())) { if (log.isDebugEnabled()) log.debug("Serving mapped URI from DataManager cache: {}", targetURI); - Model model = getSystem().getDataManager().loadModel(targetURI.toString()); + Model model = org.apache.jena.rdf.model.ModelFactory.createModelForGraph(getSystem().getRepository().get(targetURI.toString())); requestContext.abortWith(getResponse(model, Response.Status.OK)); return; } @@ -195,7 +195,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException 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().getOntModel())) + try (QueryExecution qe = QueryExecution.create(pss.asQuery(), getOntology().get())) { Model description = qe.execDescribe(); if (!description.isEmpty()) @@ -486,7 +486,7 @@ public com.atomgraph.linkeddatahub.Application getSystem() * * @return optional ontology */ - public Optional getOntology() + public Optional getOntology() { return ontology.get(); } 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 206228401..018bca6c2 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java @@ -240,7 +240,7 @@ 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 - getSystem().getOntModelSpec().getDocumentManager().getFileManager().removeCacheModel(resource.getURI()); + getSystem().getRepository().remove(resource.getURI()); } if (getApplication().isPresent() && resource.hasProperty(RDF.type, ACL.Authorization)) diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java index 0b8b65674..e8f1bcf8d 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java @@ -89,7 +89,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.jena.atlas.RuntimeIOException; import org.apache.jena.datatypes.xsd.XSDDateTime; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; import org.apache.jena.rdf.model.Model; @@ -132,7 +132,7 @@ public class DocumentHierarchyGraphStoreImpl extends com.atomgraph.core.model.im public static final String UPLOADS_PATH = "uploads"; private final com.atomgraph.linkeddatahub.apps.model.Application application; - private final Ontology ontology; + private final OntModel ontology; private final Service service; private final Providers providers; private final com.atomgraph.linkeddatahub.Application system; @@ -160,7 +160,7 @@ public class DocumentHierarchyGraphStoreImpl extends com.atomgraph.core.model.im */ @Inject public DocumentHierarchyGraphStoreImpl(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, + com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, @Context SecurityContext securityContext, Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system) { @@ -1030,7 +1030,7 @@ public com.atomgraph.linkeddatahub.apps.model.Application getApplication() * * @return ontology resource */ - public Ontology getOntology() + public OntModel getOntology() { return ontology; } diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyModelGetter.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java similarity index 64% rename from src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyModelGetter.java rename to src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java index 63c8d659d..14a0ded15 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyModelGetter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java @@ -17,56 +17,57 @@ package com.atomgraph.linkeddatahub.server.util; import com.atomgraph.client.vocabulary.LDT; +import com.atomgraph.core.client.GraphStoreClient; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import com.atomgraph.server.exception.OntologyException; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; -import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.graph.Graph; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.ModelReader; -import org.apache.jena.util.FileManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - /** - * Application's ontology model getter. - * Loads ontology model using the configured ontology query + * Ontology graph repository that resolves graphs SPARQL-first: it runs the configured ontology query + * against the admin endpoint and, only if that returns nothing, falls back to the bundled mappings / + * HTTP loading of the superclass. Replaces the legacy {@code ModelGetter} plugged into {@code OntModelSpec}. + * + * @author Martynas Jusevičius {@literal } */ -public class OntologyModelGetter implements org.apache.jena.ontology.models.ModelGetter +public class OntologyRepository extends PrefixGraphRepository { - private static final Logger log = LoggerFactory.getLogger(OntologyModelGetter.class); + private static final Logger log = LoggerFactory.getLogger(OntologyRepository.class); private final EndUserApplication app; private final com.atomgraph.linkeddatahub.Application system; - private final OntModelSpec ontModelSpec; private final Query ontologyQuery; /** - * Constructs ontology getter for application. + * Constructs the repository for an application. * * @param app end-user application resource * @param system system application - * @param ontModelSpec ontology specification + * @param gsc Graph Store client for HTTP fallback loading * @param ontologyQuery SPARQL query that loads ontology terms */ - public OntologyModelGetter(EndUserApplication app, com.atomgraph.linkeddatahub.Application system, OntModelSpec ontModelSpec, Query ontologyQuery) + public OntologyRepository(EndUserApplication app, com.atomgraph.linkeddatahub.Application system, GraphStoreClient gsc, Query ontologyQuery) { + super(gsc); this.app = app; this.system = system; - this.ontModelSpec = ontModelSpec; this.ontologyQuery = ontologyQuery; } @Override - public Model getModel(String uri) + public Graph get(String uri) { - // attempt to load ontology model from the admin endpoint. TO-DO: is that necessary if ontologies terms are now stored in a single graph? + if (isCached(uri)) return super.get(uri); + + // attempt to load the ontology from the admin endpoint ParameterizedSparqlString ontologyPss = new ParameterizedSparqlString(getOntologyQuery().toString()); ontologyPss.setIri(LDT.ontology.getLocalName(), uri); @@ -82,24 +83,15 @@ public Model getModel(String uri) model = cr.readEntity(Model.class); } - if (!model.isEmpty()) return model; - - // if SPARQL result model is empty, fallback to using FileManager - FileManager fileManager = getOntModelSpec().getDocumentManager().getFileManager(); - return fileManager.loadModel(uri); - } - - @Override - public Model getModel(String uri, ModelReader loadIfAbsent) - { - try - { - return getModel(uri); - } - catch (OntologyException ex) + if (!model.isEmpty()) { - return loadIfAbsent.readModel(ModelFactory.createDefaultModel(), uri); + Graph graph = model.getGraph(); + put(uri, graph); + return graph; } + + // if the SPARQL result is empty, fall back to bundled mappings / HTTP loading + return super.get(uri); } /** @@ -122,24 +114,14 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - /** - * Returns ontology specification. - * - * @return ontology specification - */ - public OntModelSpec getOntModelSpec() - { - return ontModelSpec; - } - /** * Returns the SPARQL query used to load ontology terms. - * + * * @return SPARQL query */ public Query getOntologyQuery() { return ontologyQuery; } - -} \ No newline at end of file + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java index 259d34a0e..c31bca208 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,8 +30,13 @@ */ public class ACL { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.w3.org/ns/auth/acl#"; @@ -49,45 +54,45 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** acl:Authorization class */ - public static final OntClass Authorization = m_model.createClass( NS + "Authorization" ); + public static final Resource Authorization = m_model.createOntClass( NS + "Authorization" ); /** acl:Read access mode */ - public static final OntClass Read = m_model.createClass( NS + "Read" ); + public static final Resource Read = m_model.createOntClass( NS + "Read" ); /** acl:Write access mode */ - public static final OntClass Write = m_model.createClass( NS + "Write" ); + public static final Resource Write = m_model.createOntClass( NS + "Write" ); /** acl:Append access mode */ - public static final OntClass Append = m_model.createClass( NS + "Append" ); + public static final Resource Append = m_model.createOntClass( NS + "Append" ); /** acl:Control access mode */ - public static final OntClass Control = m_model.createClass( NS + "Control" ); + public static final Resource Control = m_model.createOntClass( NS + "Control" ); /** acl:AuthenticatedAgent class */ - public static final OntClass AuthenticatedAgent = m_model.createClass( NS + "AuthenticatedAgent" ); + public static final Resource AuthenticatedAgent = m_model.createOntClass( NS + "AuthenticatedAgent" ); /** acl:delegates property **/ - public static final ObjectProperty delegates = m_model.createObjectProperty( NS + "delegates" ); + public static final Property delegates = m_model.createObjectProperty( NS + "delegates" ); /** acl:owner property */ - public static final ObjectProperty owner = m_model.createObjectProperty( NS + "owner" ); + public static final Property owner = m_model.createObjectProperty( NS + "owner" ); /** acl:agent property */ - public static final ObjectProperty agent = m_model.createObjectProperty( NS + "agent" ); + public static final Property agent = m_model.createObjectProperty( NS + "agent" ); /** acl:agentClass property */ - public static final ObjectProperty agentClass = m_model.createObjectProperty( NS + "agentClass" ); + public static final Property agentClass = m_model.createObjectProperty( NS + "agentClass" ); /** acl:agentGroup property */ - public static final ObjectProperty agentGroup = m_model.createObjectProperty( NS + "agentGroup" ); + public static final Property agentGroup = m_model.createObjectProperty( NS + "agentGroup" ); /** acl:mode property */ - public static final ObjectProperty mode = m_model.createObjectProperty( NS + "mode" ); + public static final Property mode = m_model.createObjectProperty( NS + "mode" ); /** acl:accessTo property */ - public static final ObjectProperty accessTo = m_model.createObjectProperty( NS + "accessTo" ); + public static final Property accessTo = m_model.createObjectProperty( NS + "accessTo" ); /** acl:accessToClass property */ - public static final ObjectProperty accessToClass = m_model.createObjectProperty( NS + "accessToClass" ); + public static final Property accessToClass = m_model.createObjectProperty( NS + "accessToClass" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java index d2bda9453..8c463d9ca 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java @@ -16,9 +16,10 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; + import org.apache.jena.rdf.model.Resource; /** @@ -28,9 +29,14 @@ */ public class Admin { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/admin#"; diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java index c0906f376..ad869bae6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -32,8 +31,13 @@ public class Cert { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.w3.org/ns/auth/cert#"; @@ -50,18 +54,18 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Public key class */ - public static final OntClass PublicKey = m_model.createClass(NS + "PublicKey"); + public static final Resource PublicKey = m_model.createOntClass(NS + "PublicKey"); /** RSA public key class */ - public static final OntClass RSAPublicKey = m_model.createClass(NS + "RSAPublicKey"); + public static final Resource RSAPublicKey = m_model.createOntClass(NS + "RSAPublicKey"); /** Key property */ - public static final ObjectProperty key = m_model.createObjectProperty( NS + "key" ); + public static final Property key = m_model.createObjectProperty( NS + "key" ); /** Modulus property */ - public static final DatatypeProperty modulus = m_model.createDatatypeProperty( NS + "modulus" ); + public static final Property modulus = m_model.createDataProperty( NS + "modulus" ); /** Exponent property */ - public static final DatatypeProperty exponent = m_model.createDatatypeProperty( NS + "exponent" ); + public static final Property exponent = m_model.createDataProperty( NS + "exponent" ); } \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java index 7ea41b305..140b2e511 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java @@ -15,12 +15,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ public class DH { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /**

The RDF model that holds the vocabulary terms

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

The namespace of the vocabulary as a string

*/ public static final String NS = "https://www.w3.org/ns/ldt/document-hierarchy#"; @@ -49,15 +53,15 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Document class */ - public static final OntClass Document = m_model.createClass( NS + "Document" ); + public static final Resource Document = m_model.createOntClass( NS + "Document" ); /** Container class */ - public static final OntClass Container = m_model.createClass( NS + "Container" ); + public static final Resource Container = m_model.createOntClass( NS + "Container" ); /** Item class */ - public static final OntClass Item = m_model.createClass( NS + "Item" ); + public static final Resource Item = m_model.createOntClass( NS + "Item" ); /** Slug property */ - public static final DatatypeProperty slug = m_model.createDatatypeProperty( NS + "slug" ); + public static final Property slug = m_model.createDataProperty( NS + "slug" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java index 03d36f429..47ba21532 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java @@ -16,10 +16,10 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; + import org.apache.jena.rdf.model.Resource; /** @@ -29,8 +29,13 @@ */ public class Default { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/default#"; @@ -50,6 +55,6 @@ public static String getURI() // DOMAIN /** Root document class */ - public static final OntClass Root = m_model.createClass(NS + "Root"); + public static final Resource Root = m_model.createOntClass(NS + "Root"); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java index a71875fe5..c2bfcc475 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public class FOAF { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://xmlns.com/foaf/0.1/"; @@ -52,45 +56,45 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Agent class */ - public static final OntClass Agent = m_model.createClass( NS + "Agent" ); + public static final Resource Agent = m_model.createOntClass( NS + "Agent" ); /** Person class */ - public static final OntClass Person = m_model.createClass( NS + "Person" ); + public static final Resource Person = m_model.createOntClass( NS + "Person" ); /** Document class */ - public static final OntClass Document = m_model.createClass( NS + "Document" ); + public static final Resource Document = m_model.createOntClass( NS + "Document" ); /** Name property */ - public static final DatatypeProperty name = m_model.createDatatypeProperty( NS + "name" ); + public static final Property name = m_model.createDataProperty( NS + "name" ); /** Given name property */ - public static final DatatypeProperty givenName = m_model.createDatatypeProperty( NS + "givenName" ); + public static final Property givenName = m_model.createDataProperty( NS + "givenName" ); /** Family name property */ - public static final DatatypeProperty familyName = m_model.createDatatypeProperty( NS + "familyName" ); + public static final Property familyName = m_model.createDataProperty( NS + "familyName" ); /** Mailbox property */ - public static final ObjectProperty mbox = m_model.createObjectProperty( NS + "mbox" ); + public static final Property mbox = m_model.createObjectProperty( NS + "mbox" ); /** Based near property */ - public static final ObjectProperty based_near = m_model.createObjectProperty( NS + "based_near" ); + public static final Property based_near = m_model.createObjectProperty( NS + "based_near" ); /** Member property */ - public static final ObjectProperty member = m_model.createObjectProperty( NS + "member" ); + public static final Property member = m_model.createObjectProperty( NS + "member" ); /** Primary topic property */ - public static final ObjectProperty primaryTopic = m_model.createObjectProperty( NS + "primaryTopic" ); + public static final Property primaryTopic = m_model.createObjectProperty( NS + "primaryTopic" ); /** Is primary topic of property */ - public static final ObjectProperty isPrimaryTopicOf = m_model.createObjectProperty( NS + "isPrimaryTopicOf" ); + public static final Property isPrimaryTopicOf = m_model.createObjectProperty( NS + "isPrimaryTopicOf" ); /** Maker property */ - public static final ObjectProperty maker = m_model.createObjectProperty( NS + "maker" ); + public static final Property maker = m_model.createObjectProperty( NS + "maker" ); /** Account property */ - public static final ObjectProperty account = m_model.createObjectProperty( NS + "account" ); + public static final Property account = m_model.createObjectProperty( NS + "account" ); /** Image property */ - public static final ObjectProperty img = m_model.createObjectProperty( NS + "img" ); + public static final Property img = m_model.createObjectProperty( NS + "img" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java index cd68c45f5..6a21142b4 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java @@ -1,9 +1,10 @@ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -14,8 +15,13 @@ public class Google { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/services/google#"; @@ -35,9 +41,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Client ID property */ - public static final DatatypeProperty clientID = m_model.createDatatypeProperty( NS + "clientID" ); + public static final Property clientID = m_model.createDataProperty( NS + "clientID" ); /** Client secret property */ - public static final DatatypeProperty clientSecret = m_model.createDatatypeProperty( NS + "clientSecret" ); + public static final Property clientSecret = m_model.createDataProperty( NS + "clientSecret" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java index fd2af9e58..bf4123de9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public class LACL { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/admin/acl#"; @@ -52,33 +56,33 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Authorization request class */ - public static final OntClass AuthorizationRequest = m_model.createClass( NS + "AuthorizationRequest" ); + public static final Resource AuthorizationRequest = m_model.createOntClass( NS + "AuthorizationRequest" ); /** Authorization request class */ - public static final OntClass OwnerAuthorization = m_model.createClass( NS + "OwnerAuthorization" ); + public static final Resource OwnerAuthorization = m_model.createOntClass( NS + "OwnerAuthorization" ); /** Password property */ - public static final DatatypeProperty password = m_model.createDatatypeProperty( NS + "password" ); + public static final Property password = m_model.createDataProperty( NS + "password" ); /** Issuer property */ - public static final DatatypeProperty issuer = m_model.createDatatypeProperty( NS + "issuer" ); + public static final Property issuer = m_model.createDataProperty( NS + "issuer" ); /** Request agent property **/ - public static final ObjectProperty requestMode = m_model.createObjectProperty( NS + "requestMode" ); + public static final Property requestMode = m_model.createObjectProperty( NS + "requestMode" ); /** Request agent property **/ - public static final ObjectProperty requestAgent = m_model.createObjectProperty( NS + "requestAgent" ); + public static final Property requestAgent = m_model.createObjectProperty( NS + "requestAgent" ); /** Request agent group property **/ - public static final ObjectProperty requestAgentGroup = m_model.createObjectProperty( NS + "requestAgentGroup" ); + public static final Property requestAgentGroup = m_model.createObjectProperty( NS + "requestAgentGroup" ); /** Request access to property */ - public static final ObjectProperty requestAccessTo = m_model.createObjectProperty( NS + "requestAccessTo" ); + public static final Property requestAccessTo = m_model.createObjectProperty( NS + "requestAccessTo" ); /** Request access to class property */ - public static final ObjectProperty requestAccessToClass = m_model.createObjectProperty( NS + "requestAccessToClass" ); + public static final Property requestAccessToClass = m_model.createObjectProperty( NS + "requestAccessToClass" ); /** Request access property */ - public static final ObjectProperty requestAccess = m_model.createObjectProperty( NS + "requestAccess" ); + public static final Property requestAccess = m_model.createObjectProperty( NS + "requestAccess" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java index 7ef0a28f1..9e991e04f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -32,8 +31,13 @@ public class LAPP { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/apps#"; @@ -53,45 +57,45 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Application class */ - public static final OntClass Context = m_model.createClass( NS + "Context" ); + public static final Resource Context = m_model.createOntClass( NS + "Context" ); /** Dataset class */ - public static final OntClass Dataset = m_model.createClass( NS + "Dataset" ); + public static final Resource Dataset = m_model.createOntClass( NS + "Dataset" ); /** Application class */ - public static final OntClass Application = m_model.createClass( NS + "Application" ); + public static final Resource Application = m_model.createOntClass( NS + "Application" ); /** Admin application class */ - public static final OntClass AdminApplication = m_model.createClass( NS + "AdminApplication" ); + public static final Resource AdminApplication = m_model.createOntClass( NS + "AdminApplication" ); /** End-user application class */ - public static final OntClass EndUserApplication = m_model.createClass( NS + "EndUserApplication" ); + public static final Resource EndUserApplication = m_model.createOntClass( NS + "EndUserApplication" ); /** Package class */ - public static final OntClass Package = m_model.createClass( NS + "Package" ); + public static final Resource Package = m_model.createOntClass( NS + "Package" ); /** Admin application class */ -// public static final ObjectProperty adminApplication = m_model.createObjectProperty( NS + "adminApplication" ); +// public static final Property adminApplication = m_model.createObjectProperty( NS + "adminApplication" ); // // /** End-user application class */ -// public static final ObjectProperty endUserApplication = m_model.createObjectProperty( NS + "endUserApplication" ); +// public static final Property endUserApplication = m_model.createObjectProperty( NS + "endUserApplication" ); /** Frontend proxy property */ - public static final ObjectProperty frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); + public static final Property frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); /** Backend proxy property */ - public static final ObjectProperty backendProxy = m_model.createObjectProperty( NS + "backendProxy" ); + public static final Property backendProxy = m_model.createObjectProperty( NS + "backendProxy" ); /** Prefix property */ - public static final ObjectProperty prefix = m_model.createObjectProperty( NS + "prefix" ); + public static final Property prefix = m_model.createObjectProperty( NS + "prefix" ); /** Read-only property */ - public static final DatatypeProperty allowRead = m_model.createDatatypeProperty( NS + "allowRead" ); + public static final Property allowRead = m_model.createDataProperty( NS + "allowRead" ); /** Origin property for subdomain-based application matching */ - public static final ObjectProperty origin = m_model.createObjectProperty(NS + "origin"); + public static final Property origin = m_model.createObjectProperty(NS + "origin"); /** Application property (for Link header rel) */ - public static final ObjectProperty application = m_model.createObjectProperty( NS + "application" ); + public static final Property application = m_model.createObjectProperty( NS + "application" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java index 629fe4d4a..a55c1fbce 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public class LDH { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub#"; @@ -51,62 +55,62 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Dataset class */ - public static final OntClass Dataset = m_model.createClass(NS + "Dataset"); + public static final Resource Dataset = m_model.createOntClass(NS + "Dataset"); /** Generic service class */ - public static final OntClass GenericService = m_model.createClass(NS + "GenericService"); + public static final Resource GenericService = m_model.createOntClass(NS + "GenericService"); /** Import class */ - public static final OntClass Import = m_model.createClass(NS + "Import"); + public static final Resource Import = m_model.createOntClass(NS + "Import"); /** CSV import class */ - public static final OntClass CSVImport = m_model.createClass(NS + "CSVImport"); + public static final Resource CSVImport = m_model.createOntClass(NS + "CSVImport"); /** RDF import class */ - public static final OntClass RDFImport = m_model.createClass(NS + "RDFImport"); + public static final Resource RDFImport = m_model.createOntClass(NS + "RDFImport"); /** File class */ - public static final OntClass File = m_model.createClass(NS + "File"); + public static final Resource File = m_model.createOntClass(NS + "File"); /** Object class */ - public static final OntClass Object = m_model.createClass(NS + "Object"); + public static final Resource Object = m_model.createOntClass(NS + "Object"); /** View class */ - public static final OntClass View = m_model.createClass(NS + "View"); + public static final Resource View = m_model.createOntClass(NS + "View"); /** URI syntax violation class */ - public static final OntClass URISyntaxViolation = m_model.createClass(NS + "URISyntaxViolation"); + public static final Resource URISyntaxViolation = m_model.createOntClass(NS + "URISyntaxViolation"); /** Base property */ - public static final ObjectProperty base = m_model.createObjectProperty( NS + "base" ); + public static final Property base = m_model.createObjectProperty( NS + "base" ); /** File property */ - public static final ObjectProperty file = m_model.createObjectProperty( NS + "file" ); + public static final Property file = m_model.createObjectProperty( NS + "file" ); /** Action property */ - public static final ObjectProperty action = m_model.createObjectProperty( NS + "action" ); + public static final Property action = m_model.createObjectProperty( NS + "action" ); /** Delimiter property */ - public static final DatatypeProperty delimiter = m_model.createDatatypeProperty( NS + "delimiter" ); + public static final Property delimiter = m_model.createDataProperty( NS + "delimiter" ); /** Violation value property */ - public static final DatatypeProperty violationValue = m_model.createDatatypeProperty( NS + "violationValue" ); + public static final Property violationValue = m_model.createDataProperty( NS + "violationValue" ); /** Request URI property */ - public static final ObjectProperty requestUri = m_model.createObjectProperty(NS + "requestUri"); + public static final Property requestUri = m_model.createObjectProperty(NS + "requestUri"); /** HTTP headers property */ - public static final ObjectProperty httpHeaders = m_model.createObjectProperty(NS + "httpHeaders"); + public static final Property httpHeaders = m_model.createObjectProperty(NS + "httpHeaders"); /** Service property */ - public static final ObjectProperty service = m_model.createObjectProperty( NS + "service" ); + public static final Property service = m_model.createObjectProperty( NS + "service" ); /** * For shape property */ - public static final ObjectProperty forShape = m_model.createObjectProperty( NS + "forShape" ); + public static final Property forShape = m_model.createObjectProperty( NS + "forShape" ); /** * Import property - used to import packages into an application */ - public static final ObjectProperty importPackage = m_model.createObjectProperty( NS + "import" ); + public static final Property importPackage = m_model.createObjectProperty( NS + "import" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java index 5daf5c389..e808098ae 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,8 +30,13 @@ */ public class LDHC { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/config#"; @@ -51,138 +56,138 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Base URI property */ - public static final ObjectProperty baseUri = m_model.createObjectProperty( NS + "baseUri" ); + public static final Property baseUri = m_model.createObjectProperty( NS + "baseUri" ); /** Proxy scheme property */ - public static final DatatypeProperty proxyScheme = m_model.createDatatypeProperty( NS + "proxyScheme" ); + public static final Property proxyScheme = m_model.createDataProperty( NS + "proxyScheme" ); /** Proxy host property */ - public static final DatatypeProperty proxyHost = m_model.createDatatypeProperty( NS + "proxyHost" ); + public static final Property proxyHost = m_model.createDataProperty( NS + "proxyHost" ); /** Proxy port property */ - public static final DatatypeProperty proxyPort = m_model.createDatatypeProperty( NS + "proxyPort" ); + public static final Property proxyPort = m_model.createDataProperty( NS + "proxyPort" ); /** Document type query property */ - public static final DatatypeProperty documentTypeQuery = m_model.createDatatypeProperty( NS + "documentTypeQuery" ); + public static final Property documentTypeQuery = m_model.createDataProperty( NS + "documentTypeQuery" ); /** Document owner query property */ - public static final DatatypeProperty documentOwnerQuery = m_model.createDatatypeProperty( NS + "documentOwnerQuery" ); + public static final Property documentOwnerQuery = m_model.createDataProperty( NS + "documentOwnerQuery" ); /** ACL query property */ - public static final DatatypeProperty aclQuery = m_model.createDatatypeProperty( NS + "aclQuery" ); + public static final Property aclQuery = m_model.createDataProperty( NS + "aclQuery" ); /** Owner's ACL property */ - public static final DatatypeProperty ownerAclQuery = m_model.createDatatypeProperty( NS + "ownerAclQuery" ); + public static final Property ownerAclQuery = m_model.createDataProperty( NS + "ownerAclQuery" ); /** WebID query property */ - public static final DatatypeProperty webIDQuery = m_model.createDatatypeProperty( NS + "webIDQuery" ); + public static final Property webIDQuery = m_model.createDataProperty( NS + "webIDQuery" ); /** Agent query property */ - public static final DatatypeProperty agentQuery = m_model.createDatatypeProperty( NS + "agentQuery" ); + public static final Property agentQuery = m_model.createDataProperty( NS + "agentQuery" ); /** User account query property */ - public static final DatatypeProperty userAccountQuery = m_model.createDatatypeProperty( NS + "userAccountQuery" ); + public static final Property userAccountQuery = m_model.createDataProperty( NS + "userAccountQuery" ); /** Ontology query property */ - public static final DatatypeProperty ontologyQuery = m_model.createDatatypeProperty( NS + "ontologyQuery" ); + public static final Property ontologyQuery = m_model.createDataProperty( NS + "ontologyQuery" ); /** Upload root property */ - public static final ObjectProperty uploadRoot = m_model.createObjectProperty( NS + "uploadRoot" ); + public static final Property uploadRoot = m_model.createObjectProperty( NS + "uploadRoot" ); /** Invalidate cache property */ - public static final DatatypeProperty invalidateCache = m_model.createDatatypeProperty( NS + "invalidateCache" ); + public static final Property invalidateCache = m_model.createDataProperty( NS + "invalidateCache" ); /** Cookie max age property */ - public static final DatatypeProperty cookieMaxAge = m_model.createDatatypeProperty( NS + "cookieMaxAge" ); + public static final Property cookieMaxAge = m_model.createDataProperty( NS + "cookieMaxAge" ); /** Client keystore property */ - public static final ObjectProperty clientKeyStore = m_model.createObjectProperty( NS + "clientKeyStore" ); + public static final Property clientKeyStore = m_model.createObjectProperty( NS + "clientKeyStore" ); /** Client keystore password property */ - public static final DatatypeProperty clientKeyStorePassword = m_model.createDatatypeProperty( NS + "clientKeyStorePassword" ); + public static final Property clientKeyStorePassword = m_model.createDataProperty( NS + "clientKeyStorePassword" ); /** Secretary cert alias property */ - public static final DatatypeProperty secretaryCertAlias = m_model.createDatatypeProperty( NS + "secretaryCertAlias" ); + public static final Property secretaryCertAlias = m_model.createDataProperty( NS + "secretaryCertAlias" ); /** Client truststore property */ - public static final ObjectProperty clientTrustStore = m_model.createObjectProperty( NS + "clientTrustStore" ); + public static final Property clientTrustStore = m_model.createObjectProperty( NS + "clientTrustStore" ); /** Client truststore password property */ - public static final DatatypeProperty clientTrustStorePassword = m_model.createDatatypeProperty( NS + "clientTrustStorePassword" ); + public static final Property clientTrustStorePassword = m_model.createDataProperty( NS + "clientTrustStorePassword" ); /** Signup email subject property */ - public static final DatatypeProperty signUpEMailSubject = m_model.createDatatypeProperty( NS + "signUpEMailSubject" ); + public static final Property signUpEMailSubject = m_model.createDataProperty( NS + "signUpEMailSubject" ); /** WebID signup email text property */ - public static final DatatypeProperty webIDSignUpEMailText = m_model.createDatatypeProperty( NS + "webIDSignUpEMailText" ); + public static final Property webIDSignUpEMailText = m_model.createDataProperty( NS + "webIDSignUpEMailText" ); /** OAuth signup email text property */ - public static final DatatypeProperty oAuthSignUpEMailText = m_model.createDatatypeProperty( NS + "oAuthSignUpEMailText" ); + public static final Property oAuthSignUpEMailText = m_model.createDataProperty( NS + "oAuthSignUpEMailText" ); /** Notification address property */ - public static final DatatypeProperty notificationAddress = m_model.createDatatypeProperty( NS + "notificationAddress" ); + public static final Property notificationAddress = m_model.createDataProperty( NS + "notificationAddress" ); /** Request access email subject property */ - public static final DatatypeProperty requestAccessEMailSubject = m_model.createDatatypeProperty( NS + "requestAccessEMailSubject" ); + public static final Property requestAccessEMailSubject = m_model.createDataProperty( NS + "requestAccessEMailSubject" ); /** Request access email text property */ - public static final DatatypeProperty requestAccessEMailText = m_model.createDatatypeProperty( NS + "requestAccessEMailText" ); + public static final Property requestAccessEMailText = m_model.createDataProperty( NS + "requestAccessEMailText" ); /** Authorization email subject property */ - public static final DatatypeProperty authorizationEMailSubject = m_model.createDatatypeProperty( NS + "authorizationEMailSubject" ); + public static final Property authorizationEMailSubject = m_model.createDataProperty( NS + "authorizationEMailSubject" ); /** Authorization email text property */ - public static final DatatypeProperty authorizationEMailText = m_model.createDatatypeProperty( NS + "authorizationEMailText" ); + public static final Property authorizationEMailText = m_model.createDataProperty( NS + "authorizationEMailText" ); /** Signup cert validity property */ - public static final DatatypeProperty signUpCertValidity = m_model.createDatatypeProperty( NS + "signUpCertValidity" ); + public static final Property signUpCertValidity = m_model.createDataProperty( NS + "signUpCertValidity" ); /** Context dataset property */ - public static final ObjectProperty contextDataset = m_model.createObjectProperty( NS + "contextDataset" ); + public static final Property contextDataset = m_model.createObjectProperty( NS + "contextDataset" ); /** Max connections per route property */ - public static final DatatypeProperty maxConnPerRoute = m_model.createDatatypeProperty( NS + "maxConnPerRoute" ); + public static final Property maxConnPerRoute = m_model.createDataProperty( NS + "maxConnPerRoute" ); /** Max total connections property */ - public static final DatatypeProperty maxTotalConn = m_model.createDatatypeProperty( NS + "maxTotalConn" ); + public static final Property maxTotalConn = m_model.createDataProperty( NS + "maxTotalConn" ); /** Import keep-alive property */ - public static final DatatypeProperty importKeepAlive = m_model.createDatatypeProperty( NS + "importKeepAlive" ); + public static final Property importKeepAlive = m_model.createDataProperty( NS + "importKeepAlive" ); /** HTTP client request retry count */ - public static final DatatypeProperty maxRequestRetries = m_model.createDatatypeProperty( NS + "maxRequestRetries" ); + public static final Property maxRequestRetries = m_model.createDataProperty( NS + "maxRequestRetries" ); /** Timeout in milliseconds waiting for a connection from the HTTP client pool */ - public static final DatatypeProperty connectionRequestTimeout = m_model.createDatatypeProperty( NS + "connectionRequestTimeout" ); + public static final Property connectionRequestTimeout = m_model.createDataProperty( NS + "connectionRequestTimeout" ); /** Max content length property */ - public static final DatatypeProperty maxContentLength = m_model.createDatatypeProperty( NS + "maxContentLength" ); + public static final Property maxContentLength = m_model.createDataProperty( NS + "maxContentLength" ); /** Support languages property */ - public static final DatatypeProperty supportedLanguages = m_model.createDatatypeProperty( NS + "supportedLanguages" ); + public static final Property supportedLanguages = m_model.createDataProperty( NS + "supportedLanguages" ); /** Max import threads property */ - public static final DatatypeProperty maxImportThreads = m_model.createDatatypeProperty( NS + "maxImportThreads" ); + public static final Property maxImportThreads = m_model.createDataProperty( NS + "maxImportThreads" ); /** Enable WebID signup property **/ - public static final DatatypeProperty enableWebIDSignUp = m_model.createDatatypeProperty( NS + "enableWebIDSignUp" ); + public static final Property enableWebIDSignUp = m_model.createDataProperty( NS + "enableWebIDSignUp" ); /** Enable Linked Data proxy property */ - public static final DatatypeProperty enableLinkedDataProxy = m_model.createDatatypeProperty( NS + "enableLinkedDataProxy" ); + public static final Property enableLinkedDataProxy = m_model.createDataProperty( NS + "enableLinkedDataProxy" ); /** Allow internal URLs property */ - public static final DatatypeProperty allowInternalUrls = m_model.createDatatypeProperty( NS + "allowInternalUrls" ); + public static final Property allowInternalUrls = m_model.createDataProperty( NS + "allowInternalUrls" ); /** OIDC refresh token properties property */ - public static final DatatypeProperty oidcRefreshTokens = m_model.createDatatypeProperty( NS + "oidcRefreshTokens" ); + public static final Property oidcRefreshTokens = m_model.createDataProperty( NS + "oidcRefreshTokens" ); /** Frontend proxy URI property (Varnish frontend cache, used for cache invalidation) */ - public static final ObjectProperty frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); + public static final Property frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); /** Backend proxy URI for the admin SPARQL service (used for cache invalidation and endpoint URI rewriting) */ - public static final ObjectProperty backendProxyAdmin = m_model.createObjectProperty( NS + "backendProxyAdmin" ); + public static final Property backendProxyAdmin = m_model.createObjectProperty( NS + "backendProxyAdmin" ); /** Backend proxy URI for the end-user SPARQL service (used for cache invalidation and endpoint URI rewriting) */ - public static final ObjectProperty backendProxyEndUser = m_model.createObjectProperty( NS + "backendProxyEndUser" ); + public static final Property backendProxyEndUser = m_model.createObjectProperty( NS + "backendProxyEndUser" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java index e43185459..e2fdfc418 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +31,13 @@ @Deprecated public class LDHT { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/templates#"; @@ -52,9 +57,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** For class property */ - //public static final ObjectProperty forClass = m_model.createObjectProperty( NS + "forClass" ); + //public static final Property forClass = m_model.createObjectProperty( NS + "forClass" ); /** Ban property */ - public static final DatatypeProperty ban = m_model.createDatatypeProperty( NS + "ban" ); + public static final Property ban = m_model.createDataProperty( NS + "ban" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java index 6aba1781b..d828638b6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,8 +30,13 @@ */ public class NFO { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#"; @@ -51,9 +56,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** File data object class */ - public static final OntClass FileDataObject = m_model.createClass(NS + "FileDataObject"); + public static final Resource FileDataObject = m_model.createOntClass(NS + "FileDataObject"); /** Filename property */ - public static final DatatypeProperty fileName = m_model.createDatatypeProperty( NS + "fileName" ); + public static final Property fileName = m_model.createDataProperty( NS + "fileName" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java index 7998fc907..c64923c8e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java @@ -1,9 +1,10 @@ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -14,8 +15,13 @@ public class ORCID { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/services/orcid#"; @@ -35,9 +41,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Client ID property */ - public static final DatatypeProperty clientID = m_model.createDatatypeProperty( NS + "clientID" ); + public static final Property clientID = m_model.createDataProperty( NS + "clientID" ); /** Client secret property */ - public static final DatatypeProperty clientSecret = m_model.createDatatypeProperty( NS + "clientSecret" ); + public static final Property clientSecret = m_model.createDataProperty( NS + "clientSecret" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java index efd61c669..fb4655c53 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,9 +29,14 @@ * @author Martynas Jusevičius {@literal } */ public class PROV { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.w3.org/ns/prov#"; @@ -52,33 +56,33 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Entity class */ - public static final OntClass Entity = m_model.createClass( NS + "Entity" ); + public static final Resource Entity = m_model.createOntClass( NS + "Entity" ); /** Activity class */ - public static final OntClass Activity = m_model.createClass( NS + "Activity" ); + public static final Resource Activity = m_model.createOntClass( NS + "Activity" ); /** Agent class */ - public static final OntClass Agent = m_model.createClass( NS + "Agent" ); + public static final Resource Agent = m_model.createOntClass( NS + "Agent" ); /** Was attributed to property */ - public static final ObjectProperty wasAttributedTo = m_model.createObjectProperty( NS + "wasAttributedTo" ); + public static final Property wasAttributedTo = m_model.createObjectProperty( NS + "wasAttributedTo" ); /** Was derived from property */ - public static final ObjectProperty wasDerivedFrom = m_model.createObjectProperty( NS + "wasDerivedFrom" ); + public static final Property wasDerivedFrom = m_model.createObjectProperty( NS + "wasDerivedFrom" ); /** Was generated by property */ - public static final ObjectProperty wasGeneratedBy = m_model.createObjectProperty( NS + "wasGeneratedBy" ); + public static final Property wasGeneratedBy = m_model.createObjectProperty( NS + "wasGeneratedBy" ); /** Was started by property */ - public static final ObjectProperty wasStartedBy = m_model.createObjectProperty( NS + "wasStartedBy" ); + public static final Property wasStartedBy = m_model.createObjectProperty( NS + "wasStartedBy" ); /** Started at time property */ - public static final DatatypeProperty startedAtTime = m_model.createDatatypeProperty( NS + "startedAtTime" ); + public static final Property startedAtTime = m_model.createDataProperty( NS + "startedAtTime" ); /** Ended at time property */ - public static final DatatypeProperty endedAtTime = m_model.createDatatypeProperty( NS + "endedAtTime" ); + public static final Property endedAtTime = m_model.createDataProperty( NS + "endedAtTime" ); /** Generated at time property */ - public static final DatatypeProperty generatedAtTime = m_model.createDatatypeProperty( NS + "generatedAtTime" ); + public static final Property generatedAtTime = m_model.createDataProperty( NS + "generatedAtTime" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java index 302ef9f39..6e2f11f0b 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java @@ -16,8 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.*; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -27,12 +30,17 @@ */ public class SIOC { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** *

* The ontology model that holds the vocabulary terms *

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

@@ -74,7 +82,7 @@ public static String getURI() { * etc. *

*/ - public static final ObjectProperty ABOUT = m_model + public static final Property ABOUT = m_model .createObjectProperty("http://rdfs.org/sioc/ns#about"); /** @@ -82,7 +90,7 @@ public static String getURI() { * Refers to the foaf:Agent or foaf:Person who owns this sioc:User online account. *

*/ - public static final ObjectProperty ACCOUNT_OF = m_model + public static final Property ACCOUNT_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#account_of"); /** @@ -90,7 +98,7 @@ public static String getURI() { * A Site that the User is an administrator of. *

*/ - public static final ObjectProperty ADMINISTRATOR_OF = m_model + public static final Property ADMINISTRATOR_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#administrator_of"); /** @@ -98,7 +106,7 @@ public static String getURI() { * The URI of a file attached to an Item. *

*/ - public static final ObjectProperty ATTACHMENT = m_model + public static final Property ATTACHMENT = m_model .createObjectProperty("http://rdfs.org/sioc/ns#attachment"); /** @@ -106,7 +114,7 @@ public static String getURI() { * An image or depiction used to represent this User. *

*/ - public static final ObjectProperty AVATAR = m_model + public static final Property AVATAR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#avatar"); /** @@ -114,7 +122,7 @@ public static String getURI() { * An Item that this Container contains. *

*/ - public static final ObjectProperty CONTAINER_OF = m_model + public static final Property CONTAINER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#container_of"); /** @@ -122,7 +130,7 @@ public static String getURI() { * A resource that the User is a creator of. *

*/ - public static final ObjectProperty CREATOR_OF = m_model + public static final Property CREATOR_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#creator_of"); /** @@ -130,7 +138,7 @@ public static String getURI() { * An electronic mail address of the User. *

*/ - public static final ObjectProperty EMAIL = m_model + public static final Property EMAIL = m_model .createObjectProperty("http://rdfs.org/sioc/ns#email"); /** @@ -138,7 +146,7 @@ public static String getURI() { * A feed (e.g. RSS, Atom, etc.) pertaining to this resource (e.g. for a Forum, Site, User, etc.). *

*/ - public static final ObjectProperty FEED = m_model + public static final Property FEED = m_model .createObjectProperty("http://rdfs.org/sioc/ns#feed"); /** @@ -147,7 +155,7 @@ public static String getURI() { * updates). *

*/ - public static final ObjectProperty FOLLOWS = m_model + public static final Property FOLLOWS = m_model .createObjectProperty("http://rdfs.org/sioc/ns#follows"); /** @@ -155,13 +163,13 @@ public static String getURI() { * A User who has this Role. *

*/ - public static final ObjectProperty FUNCTION_OF = m_model + public static final Property FUNCTION_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#function_of"); /** * This property has been renamed. Use sioc:sioc:usergroup_of instead. */ - public static final ObjectProperty GROUP_OF = m_model + public static final Property GROUP_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#group_of"); /** @@ -169,7 +177,7 @@ public static String getURI() { * A User who is an administrator of this Site. *

*/ - public static final ObjectProperty HAS_ADMINISTRATOR = m_model + public static final Property HAS_ADMINISTRATOR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_administrator"); /** @@ -177,7 +185,7 @@ public static String getURI() { * The Container to which this Item belongs. *

*/ - public static final ObjectProperty HAS_CONTAINER = m_model + public static final Property HAS_CONTAINER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_container"); /** @@ -185,7 +193,7 @@ public static String getURI() { * This is the User who made this resource. *

*/ - public static final ObjectProperty HAS_CREATOR = m_model + public static final Property HAS_CREATOR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_creator"); /** @@ -193,7 +201,7 @@ public static String getURI() { * The discussion that is related to this Item. *

*/ - public static final ObjectProperty HAS_DISCUSSION = m_model + public static final Property HAS_DISCUSSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_discussion"); /** @@ -201,13 +209,13 @@ public static String getURI() { * A Role that this User has. *

*/ - public static final ObjectProperty HAS_FUNCTION = m_model + public static final Property HAS_FUNCTION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_function"); /** * This property has been renamed. Use sioc:has_usergroup instead. */ - public static final ObjectProperty HAS_GROUP = m_model + public static final Property HAS_GROUP = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_group"); /** @@ -215,7 +223,7 @@ public static String getURI() { * The Site that hosts this Forum. *

*/ - public static final ObjectProperty HAS_HOST = m_model + public static final Property HAS_HOST = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_host"); /** @@ -223,7 +231,7 @@ public static String getURI() { * A User who is a member of this Usergroup. *

*/ - public static final ObjectProperty HAS_MEMBER = m_model + public static final Property HAS_MEMBER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_member"); /** @@ -231,7 +239,7 @@ public static String getURI() { * A User who is a moderator of this Forum. *

*/ - public static final ObjectProperty HAS_MODERATOR = m_model + public static final Property HAS_MODERATOR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_moderator"); /** @@ -239,7 +247,7 @@ public static String getURI() { * A User who modified this Item. *

*/ - public static final ObjectProperty HAS_MODIFIER = m_model + public static final Property HAS_MODIFIER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_modifier"); /** @@ -247,7 +255,7 @@ public static String getURI() { * A User that this resource is owned by. *

*/ - public static final ObjectProperty HAS_OWNER = m_model + public static final Property HAS_OWNER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_owner"); /** @@ -255,7 +263,7 @@ public static String getURI() { * A Container or Forum that this Container or Forum is a child of. *

*/ - public static final ObjectProperty HAS_PARENT = m_model + public static final Property HAS_PARENT = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_parent"); /** @@ -263,7 +271,7 @@ public static String getURI() { * An resource that is a part of this subject. *

*/ - public static final ObjectProperty HAS_PART = m_model + public static final Property HAS_PART = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_part"); /** @@ -271,7 +279,7 @@ public static String getURI() { * Points to an Item or Post that is a reply or response to this Item or Post. *

*/ - public static final ObjectProperty HAS_REPLY = m_model + public static final Property HAS_REPLY = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_reply"); /** @@ -279,7 +287,7 @@ public static String getURI() { * A resource that this Role applies to. *

*/ - public static final ObjectProperty HAS_SCOPE = m_model + public static final Property HAS_SCOPE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_scope"); /** @@ -287,7 +295,7 @@ public static String getURI() { * A data Space which this resource is a part of. *

*/ - public static final ObjectProperty HAS_SPACE = m_model + public static final Property HAS_SPACE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_space"); /** @@ -295,7 +303,7 @@ public static String getURI() { * A User who is subscribed to this Container. *

*/ - public static final ObjectProperty HAS_SUBSCRIBER = m_model + public static final Property HAS_SUBSCRIBER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_subscriber"); /** @@ -303,7 +311,7 @@ public static String getURI() { * Points to a Usergroup that has certain access to this Space. *

*/ - public static final ObjectProperty HAS_USERGROUP = m_model + public static final Property HAS_USERGROUP = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_usergroup"); /** @@ -311,7 +319,7 @@ public static String getURI() { * A Forum that is hosted on this Site. *

*/ - public static final ObjectProperty HOST_OF = m_model + public static final Property HOST_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#host_of"); /** @@ -319,7 +327,7 @@ public static String getURI() { * Links to the latest revision of this Item or Post. *

*/ - public static final ObjectProperty LATEST_VERSION = m_model + public static final Property LATEST_VERSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#latest_version"); /** @@ -327,7 +335,7 @@ public static String getURI() { * A URI of a document which contains this SIOC object. *

*/ - public static final ObjectProperty LINK = m_model + public static final Property LINK = m_model .createObjectProperty("http://rdfs.org/sioc/ns#link"); /** @@ -335,7 +343,7 @@ public static String getURI() { * Links extracted from hyperlinks within a SIOC concept, e.g. Post or Site. *

*/ - public static final ObjectProperty LINKS_TO = m_model + public static final Property LINKS_TO = m_model .createObjectProperty("http://rdfs.org/sioc/ns#links_to"); /** @@ -343,7 +351,7 @@ public static String getURI() { * A Usergroup that this User is a member of. *

*/ - public static final ObjectProperty MEMBER_OF = m_model + public static final Property MEMBER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#member_of"); /** @@ -351,7 +359,7 @@ public static String getURI() { * A Forum that User is a moderator of. *

*/ - public static final ObjectProperty MODERATOR_OF = m_model + public static final Property MODERATOR_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#moderator_of"); /** @@ -359,7 +367,7 @@ public static String getURI() { * An Item that this User has modified. *

*/ - public static final ObjectProperty MODIFIER_OF = m_model + public static final Property MODIFIER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#modifier_of"); /** @@ -367,7 +375,7 @@ public static String getURI() { * Next Item or Post in a given Container sorted by date. *

*/ - public static final ObjectProperty NEXT_BY_DATE = m_model + public static final Property NEXT_BY_DATE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#next_by_date"); /** @@ -375,7 +383,7 @@ public static String getURI() { * Links to the next revision of this Item or Post. *

*/ - public static final ObjectProperty NEXT_VERSION = m_model + public static final Property NEXT_VERSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#next_version"); /** @@ -383,7 +391,7 @@ public static String getURI() { * A resource owned by a particular User, for example, a weblog or image gallery. *

*/ - public static final ObjectProperty OWNER_OF = m_model + public static final Property OWNER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#owner_of"); /** @@ -391,7 +399,7 @@ public static String getURI() { * A child Container or Forum that this Container or Forum is a parent of. *

*/ - public static final ObjectProperty PARENT_OF = m_model + public static final Property PARENT_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#parent_of"); /** @@ -399,7 +407,7 @@ public static String getURI() { * A resource that the subject is a part of. *

*/ - public static final ObjectProperty PART_OF = m_model + public static final Property PART_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#part_of"); /** @@ -407,7 +415,7 @@ public static String getURI() { * Previous Item or Post in a given Container sorted by date. *

*/ - public static final ObjectProperty PREVIOUS_BY_DATE = m_model + public static final Property PREVIOUS_BY_DATE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#previous_by_date"); /** @@ -415,7 +423,7 @@ public static String getURI() { * Links to the previous revision of this Item or Post. *

*/ - public static final ObjectProperty PREVIOUS_VERSION = m_model + public static final Property PREVIOUS_VERSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#previous_version"); /** @@ -423,7 +431,7 @@ public static String getURI() { * Links either created explicitly or extracted implicitly on the HTML level from the Post. *

*/ - public static final ObjectProperty REFERENCE = m_model + public static final Property REFERENCE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#reference"); /** @@ -431,7 +439,7 @@ public static String getURI() { * Related Posts for this Post, perhaps determined implicitly from topics or references. *

*/ - public static final ObjectProperty RELATED_TO = m_model + public static final Property RELATED_TO = m_model .createObjectProperty("http://rdfs.org/sioc/ns#related_to"); /** @@ -439,7 +447,7 @@ public static String getURI() { * Links to an Item or Post which this Item or Post is a reply to. *

*/ - public static final ObjectProperty REPLY_OF = m_model + public static final Property REPLY_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#reply_of"); /** @@ -447,7 +455,7 @@ public static String getURI() { * A Role that has a scope of this resource. *

*/ - public static final ObjectProperty SCOPE_OF = m_model + public static final Property SCOPE_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#scope_of"); /** @@ -455,7 +463,7 @@ public static String getURI() { * A resource which belongs to this data Space. *

*/ - public static final ObjectProperty SPACE_OF = m_model + public static final Property SPACE_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#space_of"); /** @@ -463,7 +471,7 @@ public static String getURI() { * A Container that a User is subscribed to. *

*/ - public static final ObjectProperty SUBSCRIBER_OF = m_model + public static final Property SUBSCRIBER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#subscriber_of"); /** @@ -472,7 +480,7 @@ public static String getURI() { * SKOS category. *

*/ - public static final ObjectProperty TOPIC = m_model + public static final Property TOPIC = m_model .createObjectProperty("http://rdfs.org/sioc/ns#topic"); /** @@ -480,7 +488,7 @@ public static String getURI() { * A Space that the Usergroup has access to. *

*/ - public static final ObjectProperty USERGROUP_OF = m_model + public static final Property USERGROUP_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#usergroup_of"); /** @@ -488,48 +496,48 @@ public static String getURI() { * The content of the Item in plain text format. *

*/ - public static final DatatypeProperty CONTENT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#content"); + public static final Property CONTENT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#content"); /** *

* The encoded content of the Post, contained in CDATA areas. *

*/ - public static final DatatypeProperty CONTENT_ENCODED = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#content_encoded"); + public static final Property CONTENT_ENCODED = m_model + .createDataProperty("http://rdfs.org/sioc/ns#content_encoded"); /** *

* When this was created, in ISO 8601 format. *

*/ - public static final DatatypeProperty CREATED_AT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#created_at"); + public static final Property CREATED_AT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#created_at"); /** *

* The content of the Post. *

*/ - public static final DatatypeProperty DESCRIPTION = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#description"); + public static final Property DESCRIPTION = m_model + .createDataProperty("http://rdfs.org/sioc/ns#description"); /** *

* An electronic mail address of the User, encoded using SHA1. *

*/ - public static final DatatypeProperty EMAIL_SHA1 = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#email_sha1"); + public static final Property EMAIL_SHA1 = m_model + .createDataProperty("http://rdfs.org/sioc/ns#email_sha1"); /** *

* First (real) name of this User. Synonyms include given name or christian name. *

*/ - public static final DatatypeProperty FIRST_NAME = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#first_name"); + public static final Property FIRST_NAME = m_model + .createDataProperty("http://rdfs.org/sioc/ns#first_name"); /** *

@@ -537,8 +545,8 @@ public static String getURI() { * of each type of SIOC concept within the same site. *

*/ - public static final DatatypeProperty ID = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#id"); + public static final Property ID = m_model + .createDataProperty("http://rdfs.org/sioc/ns#id"); /** *

@@ -546,47 +554,47 @@ public static String getURI() { * articles list the IP addresses for the creator or modifiers when the usernames are absent. *

*/ - public static final DatatypeProperty IP_ADDRESS = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#ip_address"); + public static final Property IP_ADDRESS = m_model + .createDataProperty("http://rdfs.org/sioc/ns#ip_address"); /** *

* Last (real) name of this user. Synonyms include surname or family name. *

*/ - public static final DatatypeProperty LAST_NAME = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#last_name"); + public static final Property LAST_NAME = m_model + .createDataProperty("http://rdfs.org/sioc/ns#last_name"); /** *

* When this was modified, in ISO 8601 format. *

*/ - public static final DatatypeProperty MODIFIED_AT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#modified_at"); + public static final Property MODIFIED_AT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#modified_at"); /** *

* The name of a SIOC instance, e.g. a username for a User, group name for a Usergroup, etc. *

*/ - public static final DatatypeProperty NAME = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#name"); + public static final Property NAME = m_model + .createDataProperty("http://rdfs.org/sioc/ns#name"); /** *

* A note associated with this resource, for example, if it has been edited by a User. *

*/ - public static final DatatypeProperty NOTE = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#note"); + public static final Property NOTE = m_model + .createDataProperty("http://rdfs.org/sioc/ns#note"); /** *

* The number of posts that this person has posted. *

*/ - public static final ObjectProperty NUM_POSTS = m_model + public static final Property NUM_POSTS = m_model .createObjectProperty("http://rdfs.org/sioc/ns#num_posts"); /** @@ -595,24 +603,24 @@ public static String getURI() { * structure is absent. *

*/ - public static final DatatypeProperty NUM_REPLIES = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#num_replies"); + public static final Property NUM_REPLIES = m_model + .createDataProperty("http://rdfs.org/sioc/ns#num_replies"); /** *

* The number of times this Item, Thread, User profile, etc. has been viewed. *

*/ - public static final DatatypeProperty NUM_VIEWS = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#num_views"); + public static final Property NUM_VIEWS = m_model + .createDataProperty("http://rdfs.org/sioc/ns#num_views"); /** *

* Keyword(s) describing subject of the Post. *

*/ - public static final DatatypeProperty SUBJECT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#subject"); + public static final Property SUBJECT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#subject"); /** *

@@ -620,8 +628,8 @@ public static String getURI() { * that has no parents, it would detail the topic thread. *

*/ - public static final DatatypeProperty TITLE = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#title"); + public static final Property TITLE = m_model + .createDataProperty("http://rdfs.org/sioc/ns#title"); // Vocabulary classes // ///////////////////////// @@ -631,42 +639,42 @@ public static String getURI() { * Community is a high-level concept that defines an online community and what it consists of. *

*/ - public static final OntClass COMMUNITY = m_model.createClass("http://rdfs.org/sioc/ns#Community"); + public static final Resource COMMUNITY = m_model.createOntClass("http://rdfs.org/sioc/ns#Community"); /** *

* An area in which content Items are contained. *

*/ - public static final OntClass CONTAINER = m_model.createClass("http://rdfs.org/sioc/ns#Container"); + public static final Resource CONTAINER = m_model.createOntClass("http://rdfs.org/sioc/ns#Container"); /** *

* A discussion area on which Posts or entries are made. *

*/ - public static final OntClass FORUM = m_model.createClass("http://rdfs.org/sioc/ns#Forum"); + public static final Resource FORUM = m_model.createOntClass("http://rdfs.org/sioc/ns#Forum"); /** *

* An Item is something which can be in a Container. *

*/ - public static final OntClass ITEM = m_model.createClass("http://rdfs.org/sioc/ns#Item"); + public static final Resource ITEM = m_model.createOntClass("http://rdfs.org/sioc/ns#Item"); /** *

* An article or message that can be posted to a Forum. *

*/ - public static final OntClass POST = m_model.createClass("http://rdfs.org/sioc/ns#Post"); + public static final Resource POST = m_model.createOntClass("http://rdfs.org/sioc/ns#Post"); /** *

* A Role is a function of a User within a scope of a particular Forum, Site, etc. *

*/ - public static final OntClass ROLE = m_model.createClass("http://rdfs.org/sioc/ns#Role"); + public static final Resource ROLE = m_model.createOntClass("http://rdfs.org/sioc/ns#Role"); /** *

@@ -675,28 +683,28 @@ public static String getURI() { * Space. *

*/ - public static final OntClass SITE = m_model.createClass("http://rdfs.org/sioc/ns#Site"); + public static final Resource SITE = m_model.createOntClass("http://rdfs.org/sioc/ns#Site"); /** *

* A Space is a place where data resides, e.g. on a website, desktop, fileshare, etc. *

*/ - public static final OntClass SPACE = m_model.createClass("http://rdfs.org/sioc/ns#Space"); + public static final Resource SPACE = m_model.createOntClass("http://rdfs.org/sioc/ns#Space"); /** *

* A container for a series of threaded discussion Posts or Items. *

*/ - public static final OntClass THREAD = m_model.createClass("http://rdfs.org/sioc/ns#Thread"); + public static final Resource THREAD = m_model.createOntClass("http://rdfs.org/sioc/ns#Thread"); /** *

* A User account in an online community site. *

*/ - public static final OntClass USER_ACCOUNT = m_model.createClass("http://rdfs.org/sioc/ns#UserAccount"); + public static final Resource USER_ACCOUNT = m_model.createOntClass("http://rdfs.org/sioc/ns#UserAccount"); /** *

@@ -704,7 +712,7 @@ public static String getURI() { * control purposes. *

*/ - public static final OntClass USERGROUP = m_model.createClass("http://rdfs.org/sioc/ns#Usergroup"); + public static final Resource USERGROUP = m_model.createOntClass("http://rdfs.org/sioc/ns#Usergroup"); // Vocabulary individuals // ///////////////////////// diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java index 9d31bc7a9..12d04d916 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,9 +29,14 @@ * @author Martynas Jusevičius {@literal } */ public class VoID { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://rdfs.org/ns/void#"; @@ -52,18 +56,18 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Dataset class */ - public static final OntClass Dataset = m_model.createClass( NS + "Dataset" ); + public static final Resource Dataset = m_model.createOntClass( NS + "Dataset" ); /** Triples property */ - public static final DatatypeProperty triples = m_model.createDatatypeProperty( NS + "triples" ); + public static final Property triples = m_model.createDataProperty( NS + "triples" ); /** Distinct subject property */ - public static final DatatypeProperty distinctSubjects = m_model.createDatatypeProperty( NS + "distinctSubjects" ); + public static final Property distinctSubjects = m_model.createDataProperty( NS + "distinctSubjects" ); /** In dataset property */ - public static final ObjectProperty inDataset = m_model.createObjectProperty( NS + "inDataset" ); + public static final Property inDataset = m_model.createObjectProperty( NS + "inDataset" ); /** Class property */ - public static final ObjectProperty _class = m_model.createObjectProperty( NS + "class" ); + public static final Property _class = m_model.createObjectProperty( NS + "class" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java b/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java index bdf1ebb5c..eb7b2daaf 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java @@ -15,7 +15,7 @@ */ package com.atomgraph.linkeddatahub.writer; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.model.auth.Agent; import com.atomgraph.linkeddatahub.server.io.ValidatingModelProvider; @@ -41,7 +41,6 @@ import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XsltExecutable; import org.apache.http.HttpHeaders; -import org.apache.jena.ontology.OntModelSpec; import org.apache.jena.rdf.model.Model; import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFWriter; @@ -70,9 +69,9 @@ public class ModelXSLTWriter extends XSLTWriterBase implements MessageBodyWriter * @param dataManager RDF data manager * @param messageDigest message digest */ - public ModelXSLTWriter(XsltExecutable xsltExec, OntModelSpec ontModelSpec, DataManager dataManager, MessageDigest messageDigest) + public ModelXSLTWriter(XsltExecutable xsltExec, RDFSourceResolver resolver, MessageDigest messageDigest) { - super(xsltExec, ontModelSpec, dataManager, messageDigest); + super(xsltExec, resolver, messageDigest); } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java b/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java index fd7b24cfe..df0d42d64 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java @@ -15,7 +15,7 @@ */ package com.atomgraph.linkeddatahub.writer; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.linkeddatahub.model.auth.Agent; import jakarta.inject.Singleton; import jakarta.ws.rs.Produces; @@ -38,7 +38,6 @@ import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XsltExecutable; import org.apache.http.HttpHeaders; -import org.apache.jena.ontology.OntModelSpec; import org.apache.jena.query.ResultSetFormatter; import org.apache.jena.query.ResultSetRewindable; import org.slf4j.Logger; @@ -65,9 +64,9 @@ public class ResultSetXSLTWriter extends XSLTWriterBase implements MessageBodyWr * @param dataManager RDF data manager * @param messageDigest message digest */ - public ResultSetXSLTWriter(XsltExecutable xsltExec, OntModelSpec ontModelSpec, DataManager dataManager, MessageDigest messageDigest) + public ResultSetXSLTWriter(XsltExecutable xsltExec, RDFSourceResolver resolver, MessageDigest messageDigest) { - super(xsltExec, ontModelSpec, dataManager, messageDigest); + super(xsltExec, resolver, messageDigest); } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java index 50fc80af8..bbdaf3442 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java @@ -15,7 +15,7 @@ */ package com.atomgraph.linkeddatahub.writer; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.linkeddatahub.writer.factory.xslt.XsltExecutableSupplier; import com.atomgraph.linkeddatahub.model.auth.Agent; @@ -58,10 +58,10 @@ import net.sf.saxon.s9api.XdmValue; import net.sf.saxon.s9api.XsltExecutable; import org.apache.http.HttpHeaders; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Model; -import org.apache.jena.riot.RDFLanguages; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,7 +88,7 @@ public abstract class XSLTWriterBase extends com.atomgraph.client.writer.XSLTWri @Inject com.atomgraph.linkeddatahub.Application system; @Inject jakarta.inject.Provider> application; - @Inject jakarta.inject.Provider dataManager; + @Inject jakarta.inject.Provider resolver; @Inject jakarta.inject.Provider xsltExecSupplier; @Inject jakarta.inject.Provider crc; @Inject jakarta.inject.Provider> authorizationContext; @@ -97,15 +97,14 @@ public abstract class XSLTWriterBase extends com.atomgraph.client.writer.XSLTWri /** * Constructs XSLT writer. - * + * * @param xsltExec compiled XSLT stylesheet - * @param ontModelSpec ontology specification - * @param dataManager RDF data manager + * @param resolver XSLT source resolver * @param messageDigest message digest */ - public XSLTWriterBase(XsltExecutable xsltExec, OntModelSpec ontModelSpec, DataManager dataManager, MessageDigest messageDigest) + public XSLTWriterBase(XsltExecutable xsltExec, RDFSourceResolver resolver, MessageDigest messageDigest) { - super(xsltExec, ontModelSpec, dataManager); // this DataManager will be unused as we override getDataManager() with the injected (subclassed) one + super(xsltExec, resolver); // this resolver is unused as we override getResolver() with the injected (request-scoped) one this.messageDigest = messageDigest; } @@ -189,7 +188,7 @@ public Map getParameters(MultivaluedMap headerMap, ObjectProperty property) + public URI getLinkURI(MultivaluedMap headerMap, Property property) { if (headerMap.get(jakarta.ws.rs.core.HttpHeaders.LINK) == null) return null; @@ -231,7 +230,10 @@ public StreamSource getSource(Model model) throws IOException try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { - model.write(stream, RDFLanguages.RDFXML.getName(), null); + RDFWriter.create(). + format(RDFFormat.RDFXML_PLAIN). + source(model). + output(stream); return new StreamSource(new ByteArrayInputStream(stream.toByteArray())); } } @@ -257,36 +259,30 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - @Override - public OntModelSpec getOntModelSpec() - { - return getSystem().getOntModelSpec(); - } - /** * Returns JAX-RS security context. - * + * * @return security context */ public SecurityContext getSecurityContext() { return securityContext; } - + @Override - public DataManager getDataManager() + public RDFSourceResolver getResolver() { - return getDataManagerProvider().get(); + return getResolverProvider().get(); } /** - * Returns a JAX-RS provider for the RDF data manager. + * Returns a JAX-RS provider for the request-scoped source resolver. * * @return provider */ - public jakarta.inject.Provider getDataManagerProvider() + public jakarta.inject.Provider getResolverProvider() { - return dataManager; + return resolver; } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/DataManagerFactory.java b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/SourceResolverFactory.java similarity index 70% rename from src/main/java/com/atomgraph/linkeddatahub/writer/factory/DataManagerFactory.java rename to src/main/java/com/atomgraph/linkeddatahub/writer/factory/SourceResolverFactory.java index a9774fd63..daca9faeb 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/DataManagerFactory.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/SourceResolverFactory.java @@ -16,18 +16,16 @@ */ package com.atomgraph.linkeddatahub.writer.factory; -import org.apache.jena.util.LocationMapper; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.ext.Provider; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.server.security.AgentContext; import com.atomgraph.linkeddatahub.vocabulary.LAPP; -import com.atomgraph.linkeddatahub.writer.impl.DataManagerImpl; -import java.net.URI; -import java.util.HashMap; +import com.atomgraph.linkeddatahub.writer.impl.SameSiteSourceResolver; import java.util.Optional; import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; @@ -40,15 +38,15 @@ import org.slf4j.LoggerFactory; /** - * JAX-RS provider for DataManager. - * + * JAX-RS provider for the request-scoped XSLT source resolver. + * * @author Martynas Jusevičius {@literal } - * @see com.atomgraph.client.util.DataManager + * @see com.atomgraph.client.util.RDFSourceResolver */ @Provider -public class DataManagerFactory implements Factory +public class SourceResolverFactory implements Factory { - private static final Logger log = LoggerFactory.getLogger(DataManagerFactory.class); + private static final Logger log = LoggerFactory.getLogger(SourceResolverFactory.class); @Context UriInfo uriInfo; @Context HttpServletRequest httpServletRequest; @@ -58,40 +56,37 @@ public class DataManagerFactory implements Factory @Inject com.atomgraph.linkeddatahub.Application system; @Override - public DataManager provide() + public RDFSourceResolver provide() { - // Always return DataManager, falling back to system DataManager if no Application (e.g., for error responses) - return getDataManager(getApplication()); + // falls back to the global repository if there is no application (e.g. for error responses) + return getResolver(getApplication()); } @Override - public void dispose(DataManager t) + public void dispose(RDFSourceResolver t) { } /** - * Returns RDF data manager. + * Returns the request-scoped source resolver, backed by the application's ontology repository + * (or the global repository) and a delegating Graph Store client. * - * @param appOpt optional end-user application (if empty, system DataManager is used) - * @return data manager + * @param appOpt optional end-user application (if empty, the global repository is used) + * @return source resolver */ - public DataManager getDataManager(Optional appOpt) + public RDFSourceResolver getResolver(Optional appOpt) { - final com.atomgraph.core.util.jena.DataManager baseManager; + final PrefixGraphRepository repository; if (appOpt.isPresent() && appOpt.get().canAs(EndUserApplication.class)) - baseManager = (com.atomgraph.core.util.jena.DataManager)getSystem().getOntModelSpec(appOpt.get().as(EndUserApplication.class)).getDocumentManager().getFileManager(); + repository = getSystem().getRepository(appOpt.get().as(EndUserApplication.class)); else - baseManager = getSystem().getDataManager(); + repository = getSystem().getRepository(); GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()). delegation(getUriInfo().getBaseUri(), getAgentContext()); - // copy cached models over from the app's FileManager - return new DataManagerImpl(LocationMapper.get(), new HashMap<>(baseManager.getModelCache()), - gsc, true, getSystem().isPreemptiveAuth(), getSystem().isResolvingUncached(), - getSystem().getBaseURI(), - getAgentContext()); + return new SameSiteSourceResolver(repository, gsc, getSystem().isResolvingUncached(), getSystem().getBaseURI()); } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/impl/DataManagerImpl.java b/src/main/java/com/atomgraph/linkeddatahub/writer/impl/SameSiteSourceResolver.java similarity index 59% rename from src/main/java/com/atomgraph/linkeddatahub/writer/impl/DataManagerImpl.java rename to src/main/java/com/atomgraph/linkeddatahub/writer/impl/SameSiteSourceResolver.java index b62cfefad..72134d5af 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/impl/DataManagerImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/impl/SameSiteSourceResolver.java @@ -16,67 +16,57 @@ */ package com.atomgraph.linkeddatahub.writer.impl; -import org.apache.jena.util.LocationMapper; -import java.net.URI; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.core.client.GraphStoreClient; -import com.atomgraph.linkeddatahub.server.security.AgentContext; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.google.common.net.InternetDomainName; -import java.util.Map; -import org.apache.jena.rdf.model.Model; +import java.net.URI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Manager for remote RDF dataset access. - * Documents can be mapped to local copies. - * + * XSLT source resolver that restricts dereferencing of uncached URIs to the same site as the + * application. Mapped (bundled) and same-site URIs resolve; arbitrary external URIs do not. + * Backed by a {@link PrefixGraphRepository}; authenticated delegation is handled by the supplied + * {@link GraphStoreClient}. + * * @author Martynas Jusevičius {@literal } */ -public class DataManagerImpl extends com.atomgraph.client.util.DataManagerImpl +public class SameSiteSourceResolver extends RDFSourceResolver { - private static final Logger log = LoggerFactory.getLogger(DataManagerImpl.class); - + private static final Logger log = LoggerFactory.getLogger(SameSiteSourceResolver.class); + private final URI rootContextURI; - private final AgentContext agentContext; /** - * Constructs RDF data manager. - * - * @param mapper location mapper - * @param modelCache model cache - * @param gsc Graph Store client - * @param cacheModelLoads true if loaded RDF models are cached - * @param preemptiveAuth true if HTTP basic auth is sent preemptively + * Constructs the resolver. + * + * @param repository graph repository (bundled/cached graphs + URI→location mapping) + * @param gsc Graph Store client (with delegation) * @param resolvingUncached true if uncached URLs are resolved * @param rootContextURI the root URI of the JAX-RS application - * @param agentContext agent context */ - public DataManagerImpl(LocationMapper mapper, Map modelCache, - GraphStoreClient gsc, - boolean cacheModelLoads, boolean preemptiveAuth, boolean resolvingUncached, - URI rootContextURI, - AgentContext agentContext) + public SameSiteSourceResolver(PrefixGraphRepository repository, GraphStoreClient gsc, boolean resolvingUncached, URI rootContextURI) { - super(mapper, modelCache, gsc, cacheModelLoads, preemptiveAuth, resolvingUncached); + super(repository, gsc, resolvingUncached); this.rootContextURI = rootContextURI; - this.agentContext = agentContext; } - + @Override - public boolean resolvingUncached(String filenameOrURI) + protected boolean resolvingUncached(String filenameOrURI) { - if (super.resolvingUncached(filenameOrURI) && !isMapped(filenameOrURI)) + if (super.resolvingUncached(filenameOrURI) && !getRepository().isMapped(filenameOrURI)) { // Allow resolving URIs from the same site (e.g., localhost:4443/static/..., admin.localhost:4443/ns) return isSameSite(getRootContextURI(), URI.create(filenameOrURI)); } - return false; // super.resolvingUncached(filenameOrURI); // configured in web.xml + return false; } /** * Checks if two URIs are from the same site (schemeful same-site). - * This allows subdomains like admin.localhost and localhost to be considered part of the same LinkedDataHub instance. + * Allows subdomains like admin.localhost and localhost to be considered part of the same instance. * Ports are ignored per the same-site definition. * * @param uri1 first URI @@ -99,12 +89,9 @@ private boolean isSameSite(URI uri1, URI uri2) InternetDomainName domain1 = InternetDomainName.from(host1); InternetDomainName domain2 = InternetDomainName.from(host2); - // For localhost domains, compare the full host (localhost == localhost, admin.localhost != localhost at domain level) - // But we want to treat them as same root domain, so just check if both end with "localhost" if (host1.equals("localhost") || host1.endsWith(".localhost")) return host2.equals("localhost") || host2.endsWith(".localhost"); - // For regular domains, compare top private domains if (domain1.isTopPrivateDomain() && domain2.isTopPrivateDomain()) return domain1.equals(domain2); if (domain1.hasPublicSuffix() && domain2.hasPublicSuffix()) @@ -114,7 +101,6 @@ private boolean isSameSite(URI uri1, URI uri2) } catch (IllegalArgumentException ex) { - // Invalid domain name, fall back to simple equality check if (log.isDebugEnabled()) log.debug("Could not parse domain names for comparison: {} and {}", host1, host2); return false; } @@ -122,7 +108,7 @@ private boolean isSameSite(URI uri1, URI uri2) /** * Returns the root URI of the JAX-RS application. - * + * * @return root URI */ public URI getRootContextURI() @@ -130,14 +116,4 @@ public URI getRootContextURI() return rootContextURI; } - /** - * Returns the agent context. - * - * @return agent context or null - */ - public AgentContext getAgentContext() - { - return agentContext; - } - -} \ No newline at end of file +} diff --git a/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java b/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java index 500fb2e5a..341e7c7eb 100644 --- a/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java +++ b/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java @@ -30,7 +30,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.Providers; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.Dataset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +52,7 @@ public class ValidatingDatasetProvider extends DatasetProvider @Context private Providers providers; - @Inject jakarta.inject.Provider> ontology; + @Inject jakarta.inject.Provider> ontology; @Override public Dataset readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException @@ -70,7 +70,7 @@ public Dataset validate(Dataset dataset) if (getOntology().isPresent()) { // SPIN validation - Validator validator = new Validator(getOntology().get().getOntModel()); + Validator validator = new Validator(getOntology().get()); List cvs = validator.validate(dataset.getDefaultModel()); if (!cvs.isEmpty()) { @@ -79,7 +79,7 @@ public Dataset validate(Dataset dataset) } // SHACL validation - Shapes shapes = Shapes.parse(getOntology().get().getOntModel().getGraph()); + Shapes shapes = Shapes.parse(getOntology().get().getGraph()); ValidationReport report = ShaclValidator.get().validate(shapes, dataset.getDefaultModel().getGraph()); if (!report.conforms()) { @@ -113,7 +113,7 @@ public Dataset validate(Dataset dataset) return dataset; } - public Optional getOntology() + public Optional getOntology() { return ontology.get(); } diff --git a/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java b/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java index 416d8ada1..1b348127b 100644 --- a/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java +++ b/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java @@ -16,7 +16,7 @@ package com.atomgraph.server.io; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import java.io.IOException; import java.io.InputStream; @@ -51,7 +51,7 @@ public class ValidatingModelProvider extends BasedModelProvider @Context private Providers providers; - @Inject jakarta.inject.Provider> ontology; + @Inject jakarta.inject.Provider> ontology; @Override public Model readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException @@ -69,7 +69,7 @@ public Model validate(Model model) if (getOntology().isPresent()) { // SPIN validation - List cvs = new Validator(getOntology().get().getOntModel()).validate(model); + List cvs = new Validator(getOntology().get()).validate(model); if (!cvs.isEmpty()) { if (log.isDebugEnabled()) log.debug("SPIN constraint violations: {}", cvs); @@ -77,7 +77,7 @@ public Model validate(Model model) } // SHACL validation - Shapes shapes = Shapes.parse(getOntology().get().getOntModel().getGraph()); + Shapes shapes = Shapes.parse(getOntology().get().getGraph()); ValidationReport report = ShaclValidator.get().validate(shapes, model.getGraph()); if (!report.conforms()) { @@ -100,7 +100,7 @@ public Model processWrite(Model model) return model; } - public Optional getOntology() + public Optional getOntology() { return ontology.get(); } diff --git a/src/main/java/com/atomgraph/server/util/Validator.java b/src/main/java/com/atomgraph/server/util/Validator.java index fc66576e5..67a6afe98 100644 --- a/src/main/java/com/atomgraph/server/util/Validator.java +++ b/src/main/java/com/atomgraph/server/util/Validator.java @@ -16,7 +16,7 @@ package com.atomgraph.server.util; -import org.apache.jena.ontology.OntModel; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import java.util.List; import org.slf4j.Logger; diff --git a/src/main/java/com/atomgraph/server/vocabulary/HTTP.java b/src/main/java/com/atomgraph/server/vocabulary/HTTP.java index e9316e1ee..316d6e3b9 100644 --- a/src/main/java/com/atomgraph/server/vocabulary/HTTP.java +++ b/src/main/java/com/atomgraph/server/vocabulary/HTTP.java @@ -16,12 +16,11 @@ package com.atomgraph.server.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -33,8 +32,13 @@ public class HTTP { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /**

The RDF model that holds the vocabulary terms

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

The namespace of the vocabulary as a string

*/ public static final String NS = "http://www.w3.org/2011/http#"; @@ -49,16 +53,16 @@ public static String getURI() /**

The namespace of the vocabulary as a resource

*/ public static final Resource NAMESPACE = m_model.createResource( NS ); - public static final OntClass Response = m_model.createClass( NS + "Response" ); + public static final Resource Response = m_model.createOntClass( NS + "Response" ); - public static final ObjectProperty sc = m_model.createObjectProperty( NS + "sc" ); + public static final Property sc = m_model.createObjectProperty( NS + "sc" ); - public static final DatatypeProperty statusCodeValue = m_model.createDatatypeProperty( NS + "statusCodeValue" ); + public static final Property statusCodeValue = m_model.createDataProperty( NS + "statusCodeValue" ); - public static final DatatypeProperty reasonPhrase = m_model.createDatatypeProperty( NS + "reasonPhrase" ); + public static final Property reasonPhrase = m_model.createDataProperty( NS + "reasonPhrase" ); - public static final DatatypeProperty absoluteURI = m_model.createDatatypeProperty( NS + "absoluteURI" ); + public static final Property absoluteURI = m_model.createDataProperty( NS + "absoluteURI" ); - public static final DatatypeProperty absolutePath = m_model.createDatatypeProperty( NS + "absolutePath" ); + public static final Property absolutePath = m_model.createDataProperty( NS + "absolutePath" ); } diff --git a/src/main/java/com/atomgraph/server/vocabulary/LDT.java b/src/main/java/com/atomgraph/server/vocabulary/LDT.java index 5b59ee7f8..28cb52bcf 100644 --- a/src/main/java/com/atomgraph/server/vocabulary/LDT.java +++ b/src/main/java/com/atomgraph/server/vocabulary/LDT.java @@ -16,12 +16,11 @@ */ package com.atomgraph.server.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public final class LDT { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /**

The RDF model that holds the vocabulary terms

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

The namespace of the vocabulary as a string

*/ public static final String NS = "https://www.w3.org/ns/ldt#"; @@ -47,51 +51,51 @@ public static String getURI() /**

The namespace of the vocabulary as a resource

*/ public static final Resource NAMESPACE = m_model.createResource( NS ); - public static final OntClass Application = m_model.createClass( NS + "Application" ); + public static final Resource Application = m_model.createOntClass( NS + "Application" ); - public static final OntClass Ontology = m_model.createClass( NS + "Ontology" ); + public static final Resource Ontology = m_model.createOntClass( NS + "Ontology" ); - public static final OntClass Template = m_model.createClass( NS + "Template" ); + public static final Resource Template = m_model.createOntClass( NS + "Template" ); - public static final OntClass Parameter = m_model.createClass( NS + "Parameter" ); + public static final Resource Parameter = m_model.createOntClass( NS + "Parameter" ); - public static final OntClass TemplateCall = m_model.createClass( NS + "TemplateCall" ); + public static final Resource TemplateCall = m_model.createOntClass( NS + "TemplateCall" ); - public static final OntClass Argument = m_model.createClass( NS + "Argument" ); + public static final Resource Argument = m_model.createOntClass( NS + "Argument" ); - public static final ObjectProperty base = m_model.createObjectProperty( NS + "base" ); + public static final Property base = m_model.createObjectProperty( NS + "base" ); - public static final ObjectProperty ontology = m_model.createObjectProperty( NS + "ontology" ); + public static final Property ontology = m_model.createObjectProperty( NS + "ontology" ); - public static final ObjectProperty service = m_model.createObjectProperty( NS + "service" ); + public static final Property service = m_model.createObjectProperty( NS + "service" ); - public static final ObjectProperty arg = m_model.createObjectProperty( NS + "arg" ); + public static final Property arg = m_model.createObjectProperty( NS + "arg" ); - public static final DatatypeProperty paramName = m_model.createDatatypeProperty( NS + "paramName" ); + public static final Property paramName = m_model.createDataProperty( NS + "paramName" ); // "extends" is a reserved keyword in Java, obviously - public static final ObjectProperty extends_ = m_model.createObjectProperty( NS + "extends" ); + public static final Property extends_ = m_model.createObjectProperty( NS + "extends" ); - public static final DatatypeProperty path = m_model.createDatatypeProperty( NS + "path" ); + public static final Property path = m_model.createDataProperty( NS + "path" ); - public static final ObjectProperty query = m_model.createObjectProperty( NS + "query" ); + public static final Property query = m_model.createObjectProperty( NS + "query" ); - public static final ObjectProperty update = m_model.createObjectProperty( NS + "update" ); + public static final Property update = m_model.createObjectProperty( NS + "update" ); - public static final DatatypeProperty match = m_model.createDatatypeProperty( NS + "match" ); + public static final Property match = m_model.createDataProperty( NS + "match" ); - public static final DatatypeProperty priority = m_model.createDatatypeProperty( NS + "priority" ); + public static final Property priority = m_model.createDataProperty( NS + "priority" ); - public static final DatatypeProperty fragment = m_model.createDatatypeProperty( NS + "fragment" ); + public static final Property fragment = m_model.createDataProperty( NS + "fragment" ); - public static final ObjectProperty param = m_model.createObjectProperty( NS + "param" ); + public static final Property param = m_model.createObjectProperty( NS + "param" ); - public static final ObjectProperty loadClass = m_model.createObjectProperty( NS + "loadClass" ); + public static final Property loadClass = m_model.createObjectProperty( NS + "loadClass" ); - public static final DatatypeProperty cacheControl = m_model.createDatatypeProperty( NS + "cacheControl" ); + public static final Property cacheControl = m_model.createDataProperty( NS + "cacheControl" ); - public static final ObjectProperty lang = m_model.createObjectProperty( NS + "lang" ); + public static final Property lang = m_model.createObjectProperty( NS + "lang" ); - public static final ObjectProperty template = m_model.createObjectProperty( NS + "template" ); + public static final Property template = m_model.createObjectProperty( NS + "template" ); } diff --git a/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl b/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl index ae9068ccf..a45a58977 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl @@ -69,7 +69,7 @@ # authorization requests -:AuthorizationRequest a rdfs:Class ; +:AuthorizationRequest a rdfs:Class, owl:Class ; spin:constructor :AuthorizationRequestConstructor ; spin:constraint :MissingRequestMode, :MissingRequestAgent, :MissingRequestAccessTo ; rdfs:label "Authorization request" ; @@ -108,7 +108,7 @@ WHERE {}""" ; rdfs:isDefinedBy : . -:CreatorAuthorization a rdfs:Class ; +:CreatorAuthorization a rdfs:Class, owl:Class ; rdfs:subClassOf acl:Authorization ; rdfs:label "Creator access" ; rdfs:comment "Creators have full control of their created resources" ; diff --git a/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl b/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl index 2c8045594..a3dae50f2 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl @@ -34,21 +34,21 @@ # CLASSES -:Document a rdfs:Class ; +:Document a rdfs:Class, owl:Class ; rdfs:subClassOf foaf:Document ; # ldt:path "{slug}/" ; rdfs:label "Document" ; # spin:constructor :DocumentConstructor ; rdfs:isDefinedBy : . -:Container a rdfs:Class ; +:Container a rdfs:Class, owl:Class ; rdfs:subClassOf :Document, sioc:Container ; # spin:constructor :ContainerConstructor ; rdfs:label "Container" ; rdfs:comment "An area in which content Items are contained" ; rdfs:isDefinedBy : . -:Item a rdfs:Class ; +:Item a rdfs:Class, owl:Class ; rdfs:subClassOf :Document, sioc:Item ; # spin:constructor :ItemConstructor ; rdfs:label "Item" ; diff --git a/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl b/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl index 5c847a307..120859f87 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl @@ -155,29 +155,29 @@ # CLASSES -:InheritedProperty a rdfs:Class ; +:InheritedProperty a rdfs:Class, owl:Class ; rdfs:subClassOf owl:AnnotationProperty ; rdfs:label "Inherited property" ; rdfs:comment "Values of this property are inherited by subclasses that do not have this property" ; rdfs:isDefinedBy : . -:Application a rdfs:Class ; +:Application a rdfs:Class, owl:Class ; rdfs:label "Application" ; rdfs:comment "Declarative Linked Data application which structure is defined by its ontology and data is access via its SPARQL service" ; rdfs:isDefinedBy : . -:Ontology a rdfs:Class ; +:Ontology a rdfs:Class, owl:Class ; rdfs:subClassOf owl:Ontology ; rdfs:label "Ontology" ; rdfs:comment "Ontology annotated with Linked Data Templates" ; rdfs:isDefinedBy : . -:Template a rdfs:Class ; +:Template a rdfs:Class, owl:Class ; rdfs:label "Resource template" ; rdfs:comment "A class of RDF resources that share the same URI template and SPARQL query template" ; rdfs:isDefinedBy : . -:Query a rdfs:Class ; +:Query a rdfs:Class, owl:Class ; rdfs:subClassOf sp:Query, [ a owl:Class ; owl:unionOf (sp:Describe sp:Construct) ; rdfs:label "Graph query forms" @@ -185,12 +185,12 @@ rdfs:label "Query" ; rdfs:isDefinedBy : . -:Update a rdfs:Class ; +:Update a rdfs:Class, owl:Class ; rdfs:subClassOf sp:Update ; rdfs:label "Update" ; rdfs:isDefinedBy : . -:Parameter a rdfs:Class ; +:Parameter a rdfs:Class, owl:Class ; rdfs:subClassOf spl:Argument ; rdfs:label "Parameter" ; rdfs:comment "Represents a query parameter that has predicate, value type, default value etc." ; diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java new file mode 100644 index 000000000..9478f75d2 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java @@ -0,0 +1,68 @@ +/** + * 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.filter.request; + +import com.atomgraph.client.util.jena.PrefixGraphRepository; +import org.apache.jena.graph.Graph; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Characterization tests for {@link OntologyFilter#addDocumentModel}, which caches an imported graph + * under a SECONDARY key: the fragment-stripped document URI. Pins the dual-key caching behavior + * retained from the legacy implementation. + * + * @author Martynas Jusevičius {@literal } + */ +public class OntologyFilterTest +{ + + /** An import URI with a fragment is also cached under its fragment-stripped document URI. */ + @Test + public void testAddDocumentModelCachesUnderStrippedDocumentURI() + { + PrefixGraphRepository repository = new PrefixGraphRepository(null); + String importURI = "http://example.org/onto#"; + String docURI = "http://example.org/onto"; + + Graph imported = ModelFactory.createDefaultModel().getGraph(); + repository.put(importURI, imported); + + OntologyFilter.addDocumentModel(repository, importURI); + + assertTrue(repository.isCached(docURI), "import graph should be cached under the document URI"); + assertSame(imported, repository.get(docURI)); + } + + /** If the document URI is mapped to a different location, the secondary cache write is skipped. */ + @Test + public void testAddDocumentModelSkipsWhenDocumentURIMapped() + { + PrefixGraphRepository repository = new PrefixGraphRepository(null); + String importURI = "http://example.org/mapped#"; + String docURI = "http://example.org/mapped"; + + repository.addLocationMapping(docURI, "file:elsewhere.ttl"); // resolve(docURI) != docURI -> skip + repository.put(importURI, ModelFactory.createDefaultModel().getGraph()); + + OntologyFilter.addDocumentModel(repository, importURI); + + assertFalse(repository.isCached(docURI), "mapped document URI should not be cached as a secondary key"); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java new file mode 100644 index 000000000..312a61415 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java @@ -0,0 +1,91 @@ +/** + * 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.filter.request; + +import com.atomgraph.client.util.jena.PrefixGraphRepository; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Pins {@link OntologyFilter#loadOntology}: it flattens the owl:imports closure into one graph, + * applies RDFS inference, and materializes the inferences into the repository cache — without ontapi + * managing a union-graph hierarchy over the shared repository (which collides on duplicate ontology IDs). + * + * @author Martynas Jusevičius {@literal } + */ +public class OntologyImportsCharacterizationTest +{ + + private static final String BASE_URI = "http://example.org/base"; + private static final String IMPORT_URI = "http://example.org/imported"; + private static final String NS = "http://example.org/ns#"; + + @Test + public void testLoadOntologyFlattensClosureWithMaterializedRDFSInference() + { + PrefixGraphRepository repository = new PrefixGraphRepository(null); + + Resource a = ResourceFactory.createResource(NS + "A"); + Resource b = ResourceFactory.createResource(NS + "B"); + Resource x = ResourceFactory.createResource(NS + "x"); + + // imported ontology: A declared as owl:Class only; B declared as rdfs:Class only (mimicking third-party vocabs like sp.ttl); + // B rdfs:subClassOf A; individual x a B + Model imported = ModelFactory.createDefaultModel(); + imported.add(imported.createResource(IMPORT_URI), RDF.type, OWL.Ontology); + imported.add(a, RDF.type, OWL.Class); + imported.add(b, RDF.type, RDFS.Class); + imported.add(b, RDFS.subClassOf, a); + imported.add(x, RDF.type, b); + repository.put(IMPORT_URI, imported.getGraph()); + + // base ontology owl:imports the imported one + Model base = ModelFactory.createDefaultModel(); + Resource baseOnt = base.createResource(BASE_URI); + base.add(baseOnt, RDF.type, OWL.Ontology); + base.add(baseOnt, OWL.imports, base.createResource(IMPORT_URI)); + repository.put(BASE_URI, base.getGraph()); + + OntologyFilter.loadOntology(repository, BASE_URI); + + Model result = ModelFactory.createModelForGraph(repository.get(BASE_URI)); + // (a) imported terms flattened into the cached graph + assertTrue(result.contains(b, RDFS.subClassOf, a), "imported terms should be flattened in"); + // (b) RDFS inference materialized as a concrete triple: x a A + assertTrue(result.contains(x, RDF.type, a), "RDFS-inferred 'x a A' should be materialized in the cached graph"); + // (c) the import is also cached under its (fragment-stripped) document URI + assertTrue(repository.isCached(IMPORT_URI), "import should remain cached"); + // (d) REGRESSION GUARD: both owl:Class and rdfs:Class-only terms must be recognized as OntClasses by the returned + // model, so GET /ns?forClass= resolves the class and runs its SPIN constructor. + // OntologyFilter promotes all rdfs:Class subjects to owl:Class so OWL2 profiles (which do not recognize bare + // rdfs:Class) can find third-party vocab terms like sp:Describe. + OntModel ontology = OntModelFactory.createModel(repository.get(BASE_URI), OntSpecification.OWL2_FULL_MEM); + assertNotNull(ontology.getOntClass(NS + "A"), "owl:Class term must be recognized as an OntClass under OWL2_FULL_MEM"); + assertNotNull(ontology.getOntClass(NS + "B"), "rdfs:Class-only term must be recognized as an OntClass after promotion"); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java new file mode 100644 index 000000000..33b3183f2 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java @@ -0,0 +1,111 @@ +/** + * 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 com.atomgraph.core.client.GraphStoreClient; +import com.atomgraph.linkeddatahub.apps.model.AdminApplication; +import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; +import com.atomgraph.linkeddatahub.model.Service; +import com.atomgraph.linkeddatahub.model.ServiceContext; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +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.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Characterization tests for {@link OntologyRepository}, which resolves an ontology graph + * SPARQL-first (admin endpoint CONSTRUCT) and falls back to the superclass (bundled mapping / HTTP) + * only when the SPARQL result is empty. + * + * @author Martynas Jusevičius {@literal } + */ +@ExtendWith(MockitoExtension.class) +public class OntologyRepositoryTest +{ + + private static final String ONTOLOGY_URI = "http://example.org/ontology#"; + private static final Query ONTOLOGY_QUERY = QueryFactory.create("CONSTRUCT { ?ontology ?p ?o } WHERE { ?ontology ?p ?o }"); + + @Mock com.atomgraph.linkeddatahub.Application system; + @Mock EndUserApplication app; + @Mock AdminApplication adminApp; + @Mock Service service; + @Mock ServiceContext serviceContext; + @Mock com.atomgraph.core.client.SPARQLClient sparqlClient; + @Mock Response response; + @Mock GraphStoreClient gsc; + + private void stubSPARQLChain(Model sparqlResult) + { + when(app.getAdminApplication()).thenReturn(adminApp); + when(adminApp.getService()).thenReturn(service); + when(system.getServiceContext(service)).thenReturn(serviceContext); + when(serviceContext.getSPARQLClient()).thenReturn(sparqlClient); + when(sparqlClient.query(any(Query.class), eq(Model.class), any(MultivaluedMap.class), any(MultivaluedMap.class))).thenReturn(response); + when(response.readEntity(Model.class)).thenReturn(sparqlResult); + } + + /** A non-empty admin SPARQL CONSTRUCT result is returned directly; the HTTP fallback is not used. */ + @Test + public void testSPARQLResultUsedWhenNonEmpty() + { + Model sparqlResult = ModelFactory.createDefaultModel(); + sparqlResult.createResource(ONTOLOGY_URI).addProperty(RDF.type, OWL.Ontology); + stubSPARQLChain(sparqlResult); + + OntologyRepository repository = new OntologyRepository(app, system, gsc, ONTOLOGY_QUERY); + Graph result = repository.get(ONTOLOGY_URI); + + assertTrue(result.contains(NodeFactory.createURI(ONTOLOGY_URI), RDF.type.asNode(), OWL.Ontology.asNode())); + verify(gsc, never()).getModel(any()); + } + + /** An empty SPARQL result falls back to the Graph Store client (HTTP) load. */ + @Test + public void testFallsBackToHttpWhenSPARQLEmpty() + { + Model empty = ModelFactory.createDefaultModel(); + stubSPARQLChain(empty); + + Model fallback = ModelFactory.createDefaultModel(); + fallback.createResource(ONTOLOGY_URI).addProperty(RDFS.label, "fallback"); + when(gsc.getModel(ONTOLOGY_URI)).thenReturn(fallback); + + OntologyRepository repository = new OntologyRepository(app, system, gsc, ONTOLOGY_QUERY); + Graph result = repository.get(ONTOLOGY_URI); + + assertTrue(result.contains(NodeFactory.createURI(ONTOLOGY_URI), RDFS.label.asNode(), NodeFactory.createLiteralString("fallback"))); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java new file mode 100644 index 000000000..6d2062f9c --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java @@ -0,0 +1,98 @@ +/** + * 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 com.atomgraph.server.util.Validator; +import com.atomgraph.spinrdf.constraints.ConstraintViolation; +import com.atomgraph.spinrdf.constraints.SPINConstraints; +import java.util.List; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Regression guard for SPIN constraint validation under the ontapi migration. + * + * {@code SPINConstraints} resolves a constraint through Jena enhanced-node polymorphism + * ({@code canAs(Query)}/{@code canAs(TemplateCall)}), which an ontapi {@link OntModel}'s profile-aware personality + * does not carry — so before the fix, validation silently fired nothing. twirl now re-bases the constraint model + * onto its SPIN personality internally (twirl {@code SPINConstraints.check}), so {@link Validator} can hand it the + * ontapi model directly. This pins that the real {@code dh:Item spin:constraint :MissingTitle} template constraint + * flags a {@code dh:Item} that lacks {@code dct:title}. + * + * @author Martynas Jusevičius {@literal } + */ +public class SPINConstraintValidationTest +{ + + static + { + org.apache.jena.sys.JenaSystem.init(); + } + + private static final String DH = "https://www.w3.org/ns/ldt/document-hierarchy#"; + + /** The full owl:imports closure the ontology pipeline assembles for the document-hierarchy ontology. */ + private OntModel loadOntology() + { + Model closure = ModelFactory.createDefaultModel(); + for (String classpath : new String[] { + "etc/sp.ttl", "etc/spl.spin.ttl", "etc/spin.ttl", + "com/atomgraph/linkeddatahub/dh.ttl", + "com/atomgraph/linkeddatahub/def.ttl", + "com/atomgraph/linkeddatahub/ldh.ttl" }) + RDFDataMgr.read(closure, classpath); + + // mirror OntologyFilter.loadOntology: RDFS-infer then materialize into a plain OWL2_FULL_MEM graph + OntModel inferred = OntModelFactory.createModel(closure.getGraph(), OntSpecification.OWL2_FULL_MEM_RDFS_INF); + OntModel materialized = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); + materialized.add(inferred); + return materialized; + } + + /** A dh:Item with NO dct:title — violates the MissingTitle constraint. */ + private Model violatingItem() + { + Model data = ModelFactory.createDefaultModel(); + data.createResource("http://example.org/doc").addProperty(RDF.type, data.createResource(DH + "Item")); + return data; + } + + @Test + public void testMissingTitleConstraintFiresThroughValidator() + { + List violations = new Validator(loadOntology()).validate(violatingItem()); + assertFalse(violations.isEmpty(), "MissingTitle SPIN constraint must fire on a dh:Item without dct:title"); + } + + @Test + public void testRawOntApiModelFiresViaTwirlRebase() + { + // The ontapi OntModel's profile-aware personality lacks the SPIN views, so canAs(...) is false on it. + // twirl's SPINConstraints.check re-bases the constraint model onto its own SPIN personality, so passing the + // ontapi model straight in still fires. Guards against twirl regressing that re-base. + List direct = SPINConstraints.check(violatingItem(), loadOntology()); + assertFalse(direct.isEmpty(), "SPINConstraints.check must fire on a raw ontapi OntModel (twirl re-bases internally)"); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java b/src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java new file mode 100644 index 000000000..95f10ddc6 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java @@ -0,0 +1,63 @@ +/** + * 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.vocabulary; + +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Characterization snapshot for the vocabulary holders (representative: {@link DH}). + * + * All ~19 vocabulary classes follow the identical pattern — a static OntModel built with + * {@code ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null)} on which + * {@code createClass} / {@code createDatatypeProperty} mint typed terms. Phase A swaps the + * factory to {@code OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM)} and renames + * {@code createClass}→{@code createOntClass}, {@code createDatatypeProperty}→{@code createDataProperty}. + * + * Asserted via the migration-stable {@link Resource} interface: the term URIs and the + * {@code rdf:type owl:Class} / {@code rdf:type owl:DatatypeProperty} triples they produce + * must stay identical after the rename. + * + * @author Martynas Jusevičius {@literal } + */ +public class VocabularyHolderTest +{ + + @Test + public void testClassTermsHaveStableUriAndType() + { + Resource document = DH.Document; + assertEquals(DH.NS + "Document", document.getURI()); + assertTrue(document.hasProperty(RDF.type, OWL.Class), "DH.Document must be typed owl:Class"); + + Resource container = DH.Container; + assertEquals(DH.NS + "Container", container.getURI()); + assertTrue(container.hasProperty(RDF.type, OWL.Class), "DH.Container must be typed owl:Class"); + } + + @Test + public void testDatatypePropertyTermHasStableUriAndType() + { + Resource slug = DH.slug; + assertEquals(DH.NS + "slug", slug.getURI()); + assertTrue(slug.hasProperty(RDF.type, OWL.DatatypeProperty), "DH.slug must be typed owl:DatatypeProperty"); + } + +} From 90cad29393404c5d576a718e0e3b513b362f6d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 22 Jun 2026 11:36:40 +0200 Subject: [PATCH 14/30] HTTP test for bnodes after PATCH --- .../PATCH-blank-node-skolemized.sh | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100755 http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh diff --git a/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh b/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh new file mode 100755 index 000000000..fec9f8343 --- /dev/null +++ b/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh @@ -0,0 +1,57 @@ +#!/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/" + +# PATCH the root with an INSERT that introduces a blank node. +# Use rdf:_99 to avoid colliding with existing rdf:_1..rdf:_8 in the test dataset. +# Expected: 204 No Content; the blank node is skolemized to a hash URI before persisting. + +update=$(cat < +PREFIX dct: + +INSERT +{ + <${END_USER_BASE_URL}> rdf:_99 _:bnode0 . + _:bnode0 dct:title "Blank node title" +} +WHERE +{} +EOF +) + +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X PATCH \ + -H "Content-Type: application/sparql-update" \ + "$END_USER_BASE_URL" \ + --data-binary "$update" \ +| grep -q "$STATUS_NO_CONTENT" + +# fetch the persisted graph and verify the blank node was skolemized to a hash URI + +response=$(curl -k -f -s -G \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H "Accept: application/n-triples" \ + "$END_USER_BASE_URL") + +rdf_99_line=$(echo "$response" | grep -E "^<${END_USER_BASE_URL}> " || true) + +[ -n "$rdf_99_line" ] || exit 1 + +# object of rdf:_99 must be a URI (<...>), not a blank node label (_:...) +! echo "$rdf_99_line" | grep -qE '_:[A-Za-z0-9]+ \.$' +echo "$rdf_99_line" | grep -qE '<\S+> \.$' From 239dcf872238653d870b128a5b7e3d038aa5df96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 22 Jun 2026 13:29:54 +0200 Subject: [PATCH 15/30] Skolemize bnodes after PATCH --- .../server/model/impl/DocumentHierarchyGraphStoreImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java index e8f1bcf8d..f82fc0c66 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java @@ -394,6 +394,7 @@ public Response patch(UpdateRequest updateRequest) Model beforeUpdateModel = ModelFactory.createDefaultModel().add(existingModel); dataset = DatasetFactory.wrap(existingModel); UpdateAction.execute(updateRequest, dataset); // update model in memory + new Skolemizer(getURI().toString()).apply(existingModel); // skolemize blank nodes introduced by INSERT // if PATCH results in an empty graph, treat it as a DELETE request if (existingModel.isEmpty()) return delete(); From 8dfd424d666971a9c181af11939d103abaad5de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 23 Jun 2026 09:40:10 +0200 Subject: [PATCH 16/30] Removed unnecessary frontend purge --- .../atomgraph/linkeddatahub/resource/admin/ClearOntology.java | 1 - 1 file changed, 1 deletion(-) 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 75ab778a9..aaa8c7fb8 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java @@ -94,7 +94,6 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer { if (log.isDebugEnabled()) log.debug("Purge ontology document with URI '{}' from frontend proxy cache", ontologyDocURI); ban(frontendProxy, ontologyDocURI.toString(), false); - xkeyPurge(frontendProxy, ontologyURI); } URI adminBackendProxy = getSystem().getServiceContext(getApplication().getService()).getBackendProxy(); if (adminBackendProxy != null) From d33e7857d40dd5fd97f3824f3c0fdda010449af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 23 Jun 2026 16:49:23 +0200 Subject: [PATCH 17/30] Set Fuseki memory limits, heap, and restart policy Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 5f3522f1d..dd1883586 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,6 +107,10 @@ services: fuseki-admin: image: atomgraph/fuseki:6.1.0 user: root # otherwise fuseki user does not have permissions to the mounted folder which is owner by root + mem_limit: 1536m # leave headroom above heap for TDB mmap/native memory + restart: on-failure + environment: + - JAVA_OPTIONS=-Xmx768m -Xms768m expose: - 3030 volumes: @@ -116,6 +120,10 @@ services: fuseki-end-user: image: atomgraph/fuseki:6.1.0 user: root # otherwise the fuseki user does not have permissions to the mounted folder which is owner by root + mem_limit: 3072m # leave headroom above heap for TDB mmap/native memory + restart: on-failure + environment: + - JAVA_OPTIONS=-Xmx1536m -Xms1536m expose: - 3030 volumes: From 4293ee911b363f794b4801d7340892358277b066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 1 Jul 2026 15:10:24 +0200 Subject: [PATCH 18/30] Removed unused imports --- src/main/java/com/atomgraph/linkeddatahub/Application.java | 4 ---- .../atomgraph/linkeddatahub/resource/admin/ClearOntology.java | 3 --- .../linkeddatahub/server/filter/request/auth/WebIDFilter.java | 1 - .../atomgraph/linkeddatahub/writer/factory/ModeFactory.java | 1 - 4 files changed, 9 deletions(-) diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 1f40e2787..2a6b94f1e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -17,13 +17,10 @@ package com.atomgraph.linkeddatahub; import com.atomgraph.client.util.jena.PrefixGraphRepository; -import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.client.util.StylesheetResolver; import com.atomgraph.linkeddatahub.writer.impl.SameSiteSourceResolver; import com.atomgraph.linkeddatahub.server.util.OntologyRepository; import org.apache.jena.riot.RDFParser; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; import com.atomgraph.linkeddatahub.server.mapper.HttpHostConnectExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.InternalURLExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.MessagingExceptionMapper; @@ -230,7 +227,6 @@ import org.apache.jena.riot.resultset.ResultSetLang; import org.apache.jena.sparql.graph.GraphReadOnly; import org.apache.jena.vocabulary.DCTerms; -import org.apache.jena.vocabulary.LocationMappingVocab; import org.apache.jena.vocabulary.RDF; import org.glassfish.hk2.api.TypeLiteral; import org.glassfish.hk2.utilities.binding.AbstractBinder; 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 aaa8c7fb8..9ce40f1a6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java @@ -31,9 +31,6 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.Resource; import org.glassfish.jersey.uri.UriComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java index b42e1fbe4..7dbfa8baf 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java @@ -39,7 +39,6 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java index e7016b906..1c70b6fb9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java @@ -18,7 +18,6 @@ import com.atomgraph.client.vocabulary.AC; import com.atomgraph.linkeddatahub.writer.Mode; -import java.util.Collections; import java.util.List; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; From 7ba5dd0cd70b7f57ba31bd2873cb35d938a40db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 14:59:25 +0200 Subject: [PATCH 19/30] Upgraded dependencies: Guava 33.6.0, twirl 2.0.0, client 5.0.1, jsoup 1.22.2, JUnit 6.1.0, Mockito 5.18.0 Pinned commons-codec 1.22.0 (version jena-base 6.1.0 declares) and excluded commons-logging from jersey-apache-connector (jcl-over-slf4j covers JCL). Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 69eaaffc1..8e891bbfa 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,19 @@ org.glassfish.jersey.connectors jersey-apache-connector 3.1.11 + + + + commons-logging + commons-logging + + + + + + commons-codec + commons-codec + 1.22.0 org.glassfish.jersey.media @@ -112,7 +125,7 @@ com.google.guava guava - 31.1-jre + 33.6.0-jre com.atomgraph.etl.csv @@ -146,14 +159,7 @@ ${project.groupId} twirl - 1.2.0-SNAPSHOT - - - - org.slf4j - slf4j-reload4j - - + 2.0.0 org.slf4j @@ -163,13 +169,13 @@ ${project.groupId} client - 4.4.0-SNAPSHOT + 5.0.1 classes ${project.groupId} client - 4.4.0-SNAPSHOT + 5.0.1 war @@ -185,24 +191,24 @@ org.jsoup jsoup - 1.22.1 + 1.22.2 org.junit.jupiter junit-jupiter - 5.11.4 + 6.1.0 test org.mockito mockito-core - 5.12.0 + 5.18.0 test org.mockito mockito-junit-jupiter - 5.12.0 + 5.18.0 test From 3fddca5eeb8b88952d24bbc7862d5014e9856c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 17:31:41 +0200 Subject: [PATCH 20/30] Web-Client bump --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8e891bbfa..6b3fffd28 100644 --- a/pom.xml +++ b/pom.xml @@ -169,13 +169,13 @@ ${project.groupId} client - 5.0.1 + 5.0.2-SNAPSHOT classes ${project.groupId} client - 5.0.1 + 5.0.2-SNAPSHOT war From a2dd80b3d5d754821a21495a25b6773b3488c04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 22:25:36 +0200 Subject: [PATCH 21/30] Re-trigger CI Co-Authored-By: Claude Opus 4.8 (1M context) From 5be0d250a7728efff3c129b0bf6367f98911a29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 22:27:21 +0200 Subject: [PATCH 22/30] Adapt to StylesheetResolver(Client) constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web-Client's StylesheetResolver now takes a plain SSL-configured JAX-RS Client instead of (PrefixGraphRepository, GraphStoreClient) — stylesheet URIs are never location-mapped and a Graph Store Protocol client has no business fetching XSLT. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/com/atomgraph/linkeddatahub/Application.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 2a6b94f1e..d73532a5a 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -857,7 +857,7 @@ protected PasswordAuthentication getPasswordAuthentication() xsltComp = xsltProc.newXsltCompiler(); xsltComp.setParameter(new QName("ldh", LDH.base.getNameSpace(), LDH.base.getLocalName()), new XdmAtomicValue(baseURI)); - xsltComp.setURIResolver(new StylesheetResolver(getRepository(), GraphStoreClient.create(client, mediaTypes))); // resolves xsl:import to raw stylesheet sources + xsltComp.setURIResolver(new StylesheetResolver(client)); // resolves xsl:import to raw stylesheet sources xsltExec = xsltComp.compile(stylesheet); } catch (FileNotFoundException ex) From 35e6b52fd29ea825a2e08cf16b4adc76142d08ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:05:38 +0200 Subject: [PATCH 23/30] Make per-app OntologyRepository creation thread-safe endUserRepositories was a plain HashMap written lazily from request threads with a check-then-put, risking map corruption and two repository instances per app (one discarded after already being cached into). ConcurrentHashMap + computeIfAbsent makes creation atomic and publication safe. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/atomgraph/linkeddatahub/Application.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index d73532a5a..e42b4c471 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -787,7 +787,7 @@ else if (app.hasProperty(RDF.type, LAPP.EndUserApplication)) serviceIt.close(); } - endUserRepositories = new HashMap<>(); + endUserRepositories = new ConcurrentHashMap<>(); // global graph repository: bundled vocabularies/ontologies mapped from the prefix-mapping config repository = new PrefixGraphRepository(GraphStoreClient.create(client, mediaTypes)); if (prefixMappingConfig != null) @@ -1788,17 +1788,15 @@ protected Map getEndUserRepositories() */ public OntologyRepository getRepository(EndUserApplication app) { - if (!getEndUserRepositories().containsKey(app.getURI())) + return getEndUserRepositories().computeIfAbsent(app.getURI(), uri -> { OntologyRepository appRepository = new OntologyRepository(app, this, GraphStoreClient.create(getClient(), getMediaTypes()), getOntologyQuery()); // seed bundled vocabulary/ontology mappings from the global repository getRepository().getLocationMappings().forEach(appRepository::addLocationMapping); getRepository().getPrefixMappings().forEach(appRepository::addPrefixMapping); - getEndUserRepositories().put(app.getURI(), appRepository); - } - - return getEndUserRepositories().get(app.getURI()); + return appRepository; + }); } /** From b1d06bfe7ebe34abfb3c9210f27bf75c39e31d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:38:39 +0200 Subject: [PATCH 24/30] Drop dead cacheModelLoads and preemptiveAuth config flags Neither is read after the Jena 6 / ont-api migration (unconditional repository caching; delegation filters handle authenticated fetches). Removes them from the Application constructor and getter, web.xml, context.xsl, entrypoint.sh (CACHE_MODEL_LOADS) and the Dockerfile ENV. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 2 -- platform/context.xsl | 4 ---- platform/entrypoint.sh | 5 ----- .../atomgraph/linkeddatahub/Application.java | 20 ++----------------- src/main/webapp/WEB-INF/web.xml | 4 ---- 5 files changed, 2 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index a0f479ddf..afcdb0e8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,8 +38,6 @@ ENV SOURCE_COMMIT=$SOURCE_COMMIT WORKDIR $CATALINA_HOME -ENV CACHE_MODEL_LOADS=true - ENV STYLESHEET=static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl ENV CACHE_STYLESHEET=true diff --git a/platform/context.xsl b/platform/context.xsl index 529c95d5d..ff80b8a23 100644 --- a/platform/context.xsl +++ b/platform/context.xsl @@ -17,7 +17,6 @@ xmlns:orcid="&orcid;" - @@ -66,9 +65,6 @@ xmlns:orcid="&orcid;" - - - diff --git a/platform/entrypoint.sh b/platform/entrypoint.sh index 929658860..d5c81dd76 100755 --- a/platform/entrypoint.sh +++ b/platform/entrypoint.sh @@ -1043,10 +1043,6 @@ if [ -n "$PROXY_PORT" ]; then PROXY_PORT_PARAM="--stringparam ldhc:proxyPort '$PROXY_PORT' " fi -if [ -n "$CACHE_MODEL_LOADS" ]; then - CACHE_MODEL_LOADS_PARAM="--stringparam a:cacheModelLoads '$CACHE_MODEL_LOADS' " -fi - # stylesheet URL must be relative to the base context URL if [ -n "$STYLESHEET" ]; then STYLESHEET_PARAM="--stringparam ac:stylesheet '$STYLESHEET' " @@ -1166,7 +1162,6 @@ fi transform="xsltproc \ --output conf/Catalina/localhost/ROOT.xml \ - $CACHE_MODEL_LOADS_PARAM \ $STYLESHEET_PARAM \ $CACHE_STYLESHEET_PARAM \ $RESOLVING_UNCACHED_PARAM \ diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index e42b4c471..b37f41e5f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -270,7 +270,6 @@ public class Application extends ResourceConfig private final Client client, externalClient, importClient, noCertClient; private final Query documentTypeQuery, documentOwnerQuery, aclQuery, ownerAclQuery, webIDQuery, agentQuery, userAccountQuery, ontologyQuery; // no relative URIs private final Integer maxGetRequestSize; - private final boolean preemptiveAuth; private final Processor xsltProc = new Processor(false); private final XsltCompiler xsltComp; private final XsltExecutable xsltExec; @@ -317,8 +316,6 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti this(servletConfig, new MediaTypes(), servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI())) : null, - servletConfig.getServletContext().getInitParameter(A.cacheModelLoads.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.cacheModelLoads.getURI())) : true, - servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI())) : false, servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()), servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) != null ? servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) : null, com.atomgraph.client.Application.getSource(servletConfig.getServletContext(), servletConfig.getServletContext().getInitParameter(AC.stylesheet.getURI()) != null ? servletConfig.getServletContext().getInitParameter(AC.stylesheet.getURI()) : null), @@ -382,9 +379,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti * @param servletConfig servlet config * @param mediaTypes supported media types * @param maxGetRequestSize maximum GET request size - * @param cacheModelLoads true if model loads should be cached - * @param preemptiveAuth true if HTTP Basic auth credentials should be sent preemptively - * @param locationMapper Jena's LocationMapper instance + * @param prefixMappingConfig location-mapping configuration source * @param contextDatasetURIString location of the context dataset * @param stylesheet stylesheet URI * @param cacheStylesheet true if stylesheet should be cached @@ -433,7 +428,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti * @param backendProxyEndUserString backend proxy URI for the end-user SPARQL service (endpoint URI rewriting + cache invalidation), or null */ public Application(final ServletConfig servletConfig, final MediaTypes mediaTypes, - final Integer maxGetRequestSize, final boolean cacheModelLoads, final boolean preemptiveAuth, + final Integer maxGetRequestSize, final String prefixMappingConfig, final String contextDatasetURIString, final Source stylesheet, final boolean cacheStylesheet, final boolean resolvingUncached, final String clientKeyStoreURIString, final String clientKeyStorePassword, @@ -574,7 +569,6 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType this.servletConfig = servletConfig; this.mediaTypes = mediaTypes; this.maxGetRequestSize = maxGetRequestSize; - this.preemptiveAuth = preemptiveAuth; this.cacheStylesheet = cacheStylesheet; this.resolvingUncached = resolvingUncached; this.enableLinkedDataProxy = enableLinkedDataProxy; @@ -1996,16 +1990,6 @@ public Integer getMaxGetRequestSize() return maxGetRequestSize; } - /** - * Returns true if HTTP Basic auth credentials should be sent preemptively. - * - * @return true if preemptively - */ - public boolean isPreemptiveAuth() - { - return preemptiveAuth; - } - /** * Returns Saxon's XSLT compiler. * diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 8bcc72883..7799b44a9 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -322,10 +322,6 @@ support@atomgraph.com]]> https://w3id.org/atomgraph/client#resolvingUncached true - - https://w3id.org/atomgraph/core#preemptiveAuth - true - https://w3id.org/atomgraph/linkeddatahub/config#invalidateCache true From 8c49aa55752e7d36e1a4ec60ec6db6a230ec81c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 5 Jul 2026 23:53:12 +0200 Subject: [PATCH 25/30] OntologyRepository: bound the ontology cache with an evicting store Override createStore() to back the per-app dynamic tier with an ExpiringMap (max 1000 entries, evict after 1h idle) instead of an unbounded map, so materialized ontologies for inactive apps are reclaimed and the cache can't grow without limit. Safe because every reader goes through OntologyFilter.getOntology(), which re-materializes the imports closure on a cache miss; reuses the net.jodah.expiringmap pattern already used for the WebID/OIDC/JWKS caches. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../server/util/OntologyRepository.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java index 14a0ded15..84bee4be6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java @@ -23,6 +23,10 @@ import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import net.jodah.expiringmap.ExpirationPolicy; +import net.jodah.expiringmap.ExpiringMap; import org.apache.jena.graph.Graph; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; @@ -62,6 +66,25 @@ public OntologyRepository(EndUserApplication app, com.atomgraph.linkeddatahub.Ap this.ontologyQuery = ontologyQuery; } + /** + * Backs the dynamically loaded graphs (materialized ontologies and their imports) with a bounded, + * idle-expiring map instead of an unbounded one. Evicted entries are transparently reloaded — the + * imports closure is re-materialized by {@code OntologyFilter} on the next cache miss — so this is a + * memory backstop, not a correctness dependency. Must not reference instance state (called during + * construction). + * + * @return evicting dynamic-graph store + */ + @Override + protected Map createStore() + { + return ExpiringMap.builder(). + maxSize(1000). + expirationPolicy(ExpirationPolicy.ACCESSED). // idle-based: keep actively-used ontologies warm + expiration(1, TimeUnit.HOURS). + build(); + } + @Override public Graph get(String uri) { From 6b5315fa08aa8912c6ae7cbcb2ec7d8c7b752bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 6 Jul 2026 00:05:48 +0200 Subject: [PATCH 26/30] OntologyRepository: serve bundled vocabularies without querying SPARQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get() ran the admin ontology CONSTRUCT before falling back to the bundled mapping, so every first access to a mapped system vocabulary (foaf, dct, acl, …) cost a wasted admin round-trip, and dataset triples under a bundled URI could shadow the shipped file. Short-circuit mapped (and already-materialized) URIs to super.get(); app ontologies still load SPARQL-first. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../linkeddatahub/server/util/OntologyRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java index 84bee4be6..5229c3822 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java @@ -88,7 +88,9 @@ protected Map createStore() @Override public Graph get(String uri) { - if (isCached(uri)) return super.get(uri); + // bundled system vocabularies (mapped to shipped files) and already-materialized ontologies bypass the + // admin SPARQL query — the bundled file is authoritative, and querying it would be a wasted round-trip + if (isMapped(uri) || isCached(uri)) return super.get(uri); // attempt to load the ontology from the admin endpoint ParameterizedSparqlString ontologyPss = new ParameterizedSparqlString(getOntologyQuery().toString()); From 5d4d91aff48cb4a074927f1d2bb85e438b9a76ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 6 Jul 2026 00:05:48 +0200 Subject: [PATCH 27/30] OntologyFilter: materialize each ontology once under a lock loadOntology is a compound load + imports-closure + RDFS inference + put, not atomic. Concurrent cold requests for the same ontology could both materialize it (duplicating SPARQL + inference) and race each other's writes. Double-check isCached under the repository lock so a single thread materializes and the others reuse the result. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../server/filter/request/OntologyFilter.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 5312cc2e5..e9bd7af71 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 @@ -146,8 +146,16 @@ 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 - if (!repository.isCached(uri)) loadOntology(repository, uri); + // 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)) + { + synchronized (repository) + { + if (!repository.isCached(uri)) loadOntology(repository, uri); + } + } return OntModelFactory.createModel(repository.get(uri), OntSpecification.OWL2_FULL_MEM); } From b53a2cfe5a15119cfe3f7def6469fe370e90eafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 6 Jul 2026 00:05:48 +0200 Subject: [PATCH 28/30] Seed the ad-hoc ontology repository in Namespace Namespace built a fresh OntologyRepository without the bundled vocabulary/ontology mappings, so an empty SPARQL result would fall through to an HTTP dereference of a bundled URI instead of the shipped file. Extract Application.createRepository(app) (the seeding done by getRepository(app)) and use it in both places. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../atomgraph/linkeddatahub/Application.java | 26 +++++++++++++------ .../linkeddatahub/resource/Namespace.java | 7 ++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index b37f41e5f..2d429ce4f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -1782,15 +1782,25 @@ protected Map getEndUserRepositories() */ public OntologyRepository getRepository(EndUserApplication app) { - return getEndUserRepositories().computeIfAbsent(app.getURI(), uri -> - { - OntologyRepository appRepository = new OntologyRepository(app, this, GraphStoreClient.create(getClient(), getMediaTypes()), getOntologyQuery()); - // seed bundled vocabulary/ontology mappings from the global repository - getRepository().getLocationMappings().forEach(appRepository::addLocationMapping); - getRepository().getPrefixMappings().forEach(appRepository::addPrefixMapping); + return getEndUserRepositories().computeIfAbsent(app.getURI(), uri -> createRepository(app)); + } - return appRepository; - }); + /** + * Creates a fresh ontology repository for an application, seeded with the bundled + * vocabulary/ontology mappings from the global repository so that mapped URIs resolve to shipped + * files rather than being dereferenced over HTTP. + * + * @param app end-user application resource + * @return ontology repository + */ + public OntologyRepository createRepository(EndUserApplication app) + { + OntologyRepository appRepository = new OntologyRepository(app, this, GraphStoreClient.create(getClient(), getMediaTypes()), getOntologyQuery()); + // seed bundled vocabulary/ontology mappings from the global repository + getRepository().getLocationMappings().forEach(appRepository::addLocationMapping); + getRepository().getPrefixMappings().forEach(appRepository::addPrefixMapping); + + return appRepository; } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java index c055f957d..5ce68929e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java @@ -139,9 +139,10 @@ 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 namespace ontology from OntDocumentManager: {}", ontologyURI); - // not returning the injected in-memory ontology because it has inferences applied to it - OntologyRepository repository = new OntologyRepository(getApplication().as(EndUserApplication.class), getSystem(), com.atomgraph.linkeddatahub.client.GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()), getSystem().getOntologyQuery()); + 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)); return getResponseBuilder(org.apache.jena.rdf.model.ModelFactory.createModelForGraph(repository.get(ontologyURI))).build(); } else throw new BadRequestException("SPARQL query string not provided"); From 0e8c831728ca86b935984fe5091d43b2790861ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 6 Jul 2026 10:52:22 +0200 Subject: [PATCH 29/30] Web-Client bump --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6b3fffd28..37bc0c6c9 100644 --- a/pom.xml +++ b/pom.xml @@ -169,13 +169,13 @@ ${project.groupId} client - 5.0.2-SNAPSHOT + 5.0.2 classes ${project.groupId} client - 5.0.2-SNAPSHOT + 5.0.2 war From 6b7277fb1264f11300f9757f1ac37d4db2a23fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 8 Jul 2026 10:12:49 +0200 Subject: [PATCH 30/30] Add ldh CLI: Java port of the bin/ HTTP API scripts Standalone Maven project under cli/ (picocli + Jena 6 + Core's GraphStoreClient). One command per script with the same options; bin/ subdirectories become nested subcommand groups. PKCS12 WebID client-cert auth, env-var defaults, shaded executable jar. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + cli/README.md | 128 ++++++++++ cli/bin/ldh | 8 + cli/pom.xml | 162 ++++++++++++ .../linkeddatahub/cli/BaseCommand.java | 209 +++++++++++++++ .../com/atomgraph/linkeddatahub/cli/LDH.java | 98 +++++++ .../cli/command/AddConstruct.java | 120 +++++++++ .../linkeddatahub/cli/command/AddFile.java | 141 ++++++++++ .../cli/command/AddGenericService.java | 112 ++++++++ .../cli/command/AddResultSetChart.java | 110 ++++++++ .../linkeddatahub/cli/command/AddSelect.java | 71 ++++++ .../linkeddatahub/cli/command/AddView.java | 101 ++++++++ .../cli/command/CreateContainer.java | 114 +++++++++ .../linkeddatahub/cli/command/CreateItem.java | 90 +++++++ .../linkeddatahub/cli/command/Delete.java | 45 ++++ .../linkeddatahub/cli/command/Get.java | 63 +++++ .../linkeddatahub/cli/command/Patch.java | 52 ++++ .../linkeddatahub/cli/command/Post.java | 54 ++++ .../linkeddatahub/cli/command/Put.java | 54 ++++ .../cli/command/admin/AddOntologyImport.java | 50 ++++ .../cli/command/admin/Admin.java | 48 ++++ .../cli/command/admin/ClearOntology.java | 55 ++++ .../cli/command/admin/acl/Acl.java | 45 ++++ .../command/admin/acl/AddAgentToGroup.java | 50 ++++ .../admin/acl/CreateAuthorization.java | 163 ++++++++++++ .../cli/command/admin/acl/CreateGroup.java | 103 ++++++++ .../cli/command/admin/acl/MakePublic.java | 59 +++++ .../command/admin/ontologies/AddClass.java | 107 ++++++++ .../admin/ontologies/AddConstructor.java | 101 ++++++++ .../ontologies/AddPropertyConstraint.java | 95 +++++++ .../admin/ontologies/AddRestriction.java | 104 ++++++++ .../command/admin/ontologies/AddSelect.java | 71 ++++++ .../admin/ontologies/CreateOntology.java | 99 ++++++++ .../admin/ontologies/ImportOntology.java | 83 ++++++ .../command/admin/ontologies/Ontologies.java | 46 ++++ .../admin/packages/InstallPackage.java | 60 +++++ .../cli/command/admin/packages/Packages.java | 45 ++++ .../admin/packages/UninstallPackage.java | 60 +++++ .../cli/command/content/AddObjectBlock.java | 121 +++++++++ .../cli/command/content/AddXHTMLBlock.java | 106 ++++++++ .../cli/command/content/Content.java | 45 ++++ .../cli/command/content/RemoveBlock.java | 50 ++++ .../cli/command/imports/AddCSVImport.java | 123 +++++++++ .../cli/command/imports/AddRDFImport.java | 124 +++++++++ .../cli/command/imports/ImportCSV.java | 81 ++++++ .../cli/command/imports/ImportRDF.java | 89 +++++++ .../cli/command/imports/Imports.java | 45 ++++ .../linkeddatahub/cli/http/ClientFactory.java | 127 +++++++++ .../linkeddatahub/cli/http/HttpException.java | 102 ++++++++ .../linkeddatahub/cli/http/LDHClient.java | 100 ++++++++ .../linkeddatahub/cli/mixin/BaseMixin.java | 59 +++++ .../cli/mixin/CertAuthMixin.java | 71 ++++++ .../linkeddatahub/cli/mixin/ProxyMixin.java | 44 ++++ .../linkeddatahub/cli/sparql/Updates.java | 151 +++++++++++ .../linkeddatahub/cli/util/Digests.java | 69 +++++ .../cli/util/SequenceNumbers.java | 75 ++++++ .../linkeddatahub/cli/util/Slugs.java | 42 +++ .../linkeddatahub/cli/util/URIRewriter.java | 104 ++++++++ .../atomgraph/linkeddatahub/cli/vocab/A.java | 42 +++ .../atomgraph/linkeddatahub/cli/vocab/AC.java | 38 +++ .../linkeddatahub/cli/vocab/ACL.java | 60 +++++ .../atomgraph/linkeddatahub/cli/vocab/DH.java | 40 +++ .../linkeddatahub/cli/vocab/LDH.java | 68 +++++ .../linkeddatahub/cli/vocab/NFO.java | 42 +++ .../atomgraph/linkeddatahub/cli/vocab/SD.java | 50 ++++ .../atomgraph/linkeddatahub/cli/vocab/SP.java | 46 ++++ .../linkeddatahub/cli/vocab/SPIN.java | 42 +++ .../linkeddatahub/cli/CommandParsingTest.java | 116 +++++++++ .../cli/command/AddFileMultiPartTest.java | 71 ++++++ .../cli/command/ModelBuildersTest.java | 240 ++++++++++++++++++ .../cli/http/ClientFactoryTest.java | 56 ++++ .../linkeddatahub/cli/sparql/UpdatesTest.java | 85 +++++++ .../cli/util/SequenceNumbersTest.java | 74 ++++++ .../cli/util/URIRewriterTest.java | 76 ++++++ cli/src/test/resources/test-keystore.p12 | Bin 0 -> 2772 bytes 75 files changed, 6051 insertions(+) create mode 100644 cli/README.md create mode 100755 cli/bin/ldh create mode 100644 cli/pom.xml create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java create mode 100644 cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java create mode 100644 cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.java create mode 100644 cli/src/test/resources/test-keystore.p12 diff --git a/.gitignore b/.gitignore index b6d335aca..0a0a72c50 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /http-tests/out /fuseki .claude/scheduled_tasks.lock +/cli/target/ diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 000000000..b37643534 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,128 @@ +# LinkedDataHub CLI + +`ldh` is a command line interface for the [LinkedDataHub](https://github.com/AtomGraph/LinkedDataHub) HTTP API. +It mirrors the shell scripts in [`bin/`](../bin) one command per script, with the same option names, +implemented in Java on top of AtomGraph [Core](https://github.com/AtomGraph/Core)'s `GraphStoreClient` +(picocli + Apache Jena). It replaces the scripts' external dependencies (`curl`, `turtle`, `python`, +`uuidgen`, `shasum`) with a single executable jar. + +## Build + +Requires Java 21 and Maven: + +```bash +cd cli +mvn package +``` + +This produces the self-contained `target/ldh.jar`. The `cli/bin/ldh` launcher runs it: + +```bash +export PATH="$PATH:$(pwd)/bin" +ldh --help +``` + +## Authentication + +Commands authenticate with a WebID client certificate from a **PKCS12 (.p12) keystore** — the format +produced by `bin/webid-keygen.sh`: + +```bash +ldh get --accept text/turtle \ + -f ssl/owner/keystore.p12 -p "$OWNER_CERT_PWD" \ + https://localhost:4443/ +``` + +Server certificates are not validated (equivalent of `curl -k`), matching the shell scripts' +behavior against self-signed development instances. + +### Environment variable defaults + +Repeated options can be set once via environment variables: + +| Variable | Option | +|---|---| +| `LDH_CERT_FILE` | `-f`, `--cert-file` | +| `LDH_CERT_PASSWORD` | `-p`, `--cert-password` | +| `LDH_BASE` | `-b`, `--base` | +| `LDH_PROXY` | `--proxy` | + +```bash +export LDH_CERT_FILE=ssl/owner/keystore.p12 LDH_CERT_PASSWORD=... LDH_BASE=https://localhost:4443/ + +ldh create-container --parent "$LDH_BASE" --title "Some" --slug some +ldh create-item --container https://localhost:4443/some/ --title "My item" --slug my-item +``` + +## Conventions + +- Commands that create or append to a document print its URL as the only line on stdout, so shell + pipelines keep working: `item=$(ldh create-item ...)`. `add-file` prints the content-addressed + upload URI (`{base}uploads/{sha1}`). All diagnostics go to stderr. +- Exit codes: `0` success, `1` HTTP error status or runtime failure (message on stderr, stack trace + with `--verbose`), `2` usage error. +- `--proxy` rewrites the request URI's origin to the proxy's origin, like the scripts do; printed + URLs keep the logical origin. +- `post`/`put` read RDF from stdin and resolve relative URIs against the target URI (the scripts' + `turtle --base` piping); `patch` reads a SPARQL 1.1 update from stdin, validates it and sends it + verbatim. + +Shell completion: `source <(ldh generate-completion)` (bash/zsh). + +## Script → command migration + +| Script | Command | +|---|---| +| `get.sh` | `ldh get` | +| `post.sh` | `ldh post` | +| `put.sh` | `ldh put` | +| `patch.sh` | `ldh patch` | +| `delete.sh` | `ldh delete` | +| `create-item.sh` | `ldh create-item` | +| `create-container.sh` | `ldh create-container` | +| `add-view.sh` | `ldh add-view` | +| `add-construct.sh` | `ldh add-construct` | +| `add-select.sh` | `ldh add-select` | +| `add-result-set-chart.sh` | `ldh add-result-set-chart` | +| `add-file.sh` | `ldh add-file` | +| `add-generic-service.sh` | `ldh add-generic-service` | +| `admin/clear-ontology.sh` | `ldh admin clear-ontology` | +| `admin/add-ontology-import.sh` | `ldh admin add-ontology-import` | +| `admin/ontologies/create-ontology.sh` | `ldh admin ontologies create-ontology` | +| `admin/ontologies/import-ontology.sh` | `ldh admin ontologies import-ontology` | +| `admin/ontologies/add-class.sh` | `ldh admin ontologies add-class` | +| `admin/ontologies/add-constructor.sh` | `ldh admin ontologies add-constructor` | +| `admin/ontologies/add-select.sh` | `ldh admin ontologies add-select` | +| `admin/ontologies/add-property-constraint.sh` | `ldh admin ontologies add-property-constraint` | +| `admin/ontologies/add-restriction.sh` | `ldh admin ontologies add-restriction` | +| `admin/acl/create-group.sh` | `ldh admin acl create-group` | +| `admin/acl/create-authorization.sh` | `ldh admin acl create-authorization` | +| `admin/acl/add-agent-to-group.sh` | `ldh admin acl add-agent-to-group` | +| `admin/acl/make-public.sh` | `ldh admin acl make-public` | +| `admin/packages/install-package.sh` | `ldh admin packages install-package` | +| `admin/packages/uninstall-package.sh` | `ldh admin packages uninstall-package` | +| `content/add-object-block.sh` | `ldh content add-object-block` | +| `content/add-xhtml-block.sh` | `ldh content add-xhtml-block` | +| `content/remove-block.sh` | `ldh content remove-block` | +| `imports/add-csv-import.sh` | `ldh imports add-csv-import` | +| `imports/add-rdf-import.sh` | `ldh imports add-rdf-import` | +| `imports/import-csv.sh` | `ldh imports import-csv` | +| `imports/import-rdf.sh` | `ldh imports import-rdf` | + +Local certificate tooling (`webid-keygen.sh`, `webid-keygen-pem.sh`, `webid-uri.sh`, +`webid-modulus.sh`, `server-cert-gen.sh`) and the experimental `sitemap/` generator remain +shell scripts. + +### Differences from the scripts + +- `-f/--cert-pem-file` is now `-f/--cert-file` and takes the `.p12` keystore directly — no + PEM conversion needed. +- `create-group` writes the `--name` value into `foaf:name`/`dct:title` (the script wrote an + unset variable, producing empty literals). +- `add-generic-service` drops the documented-but-unparsed `--slug` option. +- `add-csv-import`/`import-csv` default `--delimiter` to `,` (the script required it despite + documenting a default). +- `install-package`/`uninstall-package` print nothing on success instead of the raw HTTP status + code; use the exit code. +- `import-csv`/`import-rdf` run their steps in-process instead of spawning subscripts, and pass + `--description` through to the import metadata. diff --git a/cli/bin/ldh b/cli/bin/ldh new file mode 100755 index 000000000..93c9da987 --- /dev/null +++ b/cli/bin/ldh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Launcher for the LinkedDataHub CLI. +# Uses the jar built by `mvn package` unless LDH_JAR points elsewhere. + +dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +exec java -jar "${LDH_JAR:-$dir/../target/ldh.jar}" "$@" diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 000000000..bb7dfa747 --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + com.atomgraph + linkeddatahub-cli + 1.0.0-SNAPSHOT + jar + + LinkedDataHub CLI + Command line interface for the LinkedDataHub HTTP API + https://github.com/AtomGraph/LinkedDataHub + + + UTF-8 + 21 + 3.1.11 + + + + + info.picocli + picocli + 4.7.7 + + + com.atomgraph + core + 5.0.2 + + + + org.glassfish.jersey.containers + jersey-container-servlet + + + + + org.apache.jena + jena-arq + 6.1.0 + + + + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpclient-cache + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpclient-osgi + + + org.apache.httpcomponents + httpcore-osgi + + + + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey.version} + + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.version} + + + + jakarta.activation + jakarta.activation-api + 2.1.3 + runtime + + + org.slf4j + slf4j-nop + 2.0.17 + runtime + + + org.junit.jupiter + junit-jupiter + 5.12.2 + test + + + + + ldh + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + 21 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + com.atomgraph.linkeddatahub.cli.LDH + + true + + + + + + + META-INF/hk2-locator/default + + + + false + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + module-info.class + META-INF/versions/*/module-info.class + + + + + + + + + + + diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java new file mode 100644 index 000000000..986b32bf3 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java @@ -0,0 +1,209 @@ +/* + * 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.cli; + +import com.atomgraph.core.MediaTypes; +import com.atomgraph.linkeddatahub.cli.http.ClientFactory; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.CertAuthMixin; +import com.atomgraph.linkeddatahub.cli.mixin.ProxyMixin; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.concurrent.Callable; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFLanguages; +import org.apache.jena.riot.RDFParser; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Base class for all commands: WebID certificate authentication, proxy handling + * and shared RDF/HTTP helpers. + * + * @author Martynas Jusevičius {@literal } + */ +public abstract class BaseCommand implements Callable +{ + + /** Accepted response media type used by the scripts (Accept: text/turtle) */ + protected static final MediaType[] ACCEPT_TURTLE = { com.atomgraph.core.MediaType.TEXT_TURTLE_TYPE }; + /** Accepted response media type for content block sequence scanning */ + protected static final MediaType[] ACCEPT_NTRIPLES = { com.atomgraph.core.MediaType.APPLICATION_NTRIPLES_TYPE }; + /** Turtle request body media type */ + protected static final MediaType TEXT_TURTLE_TYPE = com.atomgraph.core.MediaType.TEXT_TURTLE_TYPE; + + @Spec + private CommandSpec spec; + + @Mixin + private CertAuthMixin certAuth; + + @Mixin + private ProxyMixin proxyMixin; + + private LDHClient client; + + /** + * Returns the lazily-built authenticated client. + * + * @return client instance + */ + protected LDHClient getClient() + { + if (client == null) + { + getCertAuth().validate(getSpec()); + client = new LDHClient(ClientFactory.createClient(getCertAuth().getCertFile(), getCertAuth().getCertPassword()), + new MediaTypes(), getEffectiveProxy()); + } + + return client; + } + + /** + * Returns the proxy URI applied to requests. Commands that target the admin application + * override this to convert the proxy to the admin subdomain. + * + * @return proxy URI or null + */ + protected URI getEffectiveProxy() + { + return getProxyMixin().getProxy(); + } + + /** + * POSTs a model to a document, failing on error status. + * + * @param client client instance + * @param target document URI + * @param model appended model + */ + protected static void post(LDHClient client, URI target, Model model) + { + HttpException.check(target, client.post(target, Entity.entity(model, TEXT_TURTLE_TYPE), ACCEPT_TURTLE)).close(); + } + + /** + * PUTs a model as a document, failing on error status. + * + * @param client client instance + * @param target document URI + * @param model document model + */ + protected static void put(LDHClient client, URI target, Model model) + { + HttpException.check(target, client.put(target, Entity.entity(model, TEXT_TURTLE_TYPE), ACCEPT_TURTLE)).close(); + } + + /** + * Returns the subject resource for an appended description: the --uri value + * resolved against the target document URI, or a fresh blank node when not given. + * + * @param model model to create the resource in + * @param target target document URI + * @param uri --uri option value (absolute or relative, can be null) + * @return subject resource + */ + protected static Resource createSubject(Model model, URI target, String uri) + { + return uri != null ? model.createResource(target.resolve(uri).toString()) : model.createResource(); + } + + /** + * Parses an RDF stream into a model, resolving relative URIs against the base URI + * (the equivalent of the scripts' turtle --base piping). + * + * @param contentType RDF media type + * @param base base URI + * @param in RDF input stream + * @return parsed model + */ + protected Model readModel(String contentType, URI base, InputStream in) + { + Lang lang = RDFLanguages.contentTypeToLang(contentType); + if (lang == null) throw new ParameterException(getSpec().commandLine(), "Unsupported RDF media type: '" + contentType + "'"); + + Model model = ModelFactory.createDefaultModel(); + RDFParser.create().source(in).lang(lang).base(base.toString()).parse(model); + return model; + } + + /** + * Streams a response body to standard output unmodified. + * + * @param response response with the body to stream + * @throws IOException stream error + */ + protected static void printBody(jakarta.ws.rs.core.Response response) throws IOException + { + try (response; InputStream is = response.readEntity(InputStream.class)) + { + is.transferTo(System.out); + System.out.flush(); + } + } + + /** + * Prints a line to standard output. Command results (created document URLs) go through here. + * + * @param value printed value + */ + protected void print(Object value) + { + getSpec().commandLine().getOut().println(value); + } + + /** + * Returns the command spec. + * + * @return command spec + */ + protected CommandSpec getSpec() + { + return spec; + } + + /** + * Returns the certificate options. + * + * @return certificate mixin + */ + protected CertAuthMixin getCertAuth() + { + return certAuth; + } + + /** + * Returns the proxy options. + * + * @return proxy mixin + */ + protected ProxyMixin getProxyMixin() + { + return proxyMixin; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java new file mode 100644 index 000000000..4f3842bbf --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java @@ -0,0 +1,98 @@ +/* + * 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.cli; + +import com.atomgraph.linkeddatahub.cli.command.AddConstruct; +import com.atomgraph.linkeddatahub.cli.command.AddFile; +import com.atomgraph.linkeddatahub.cli.command.AddGenericService; +import com.atomgraph.linkeddatahub.cli.command.AddResultSetChart; +import com.atomgraph.linkeddatahub.cli.command.AddSelect; +import com.atomgraph.linkeddatahub.cli.command.AddView; +import com.atomgraph.linkeddatahub.cli.command.CreateContainer; +import com.atomgraph.linkeddatahub.cli.command.CreateItem; +import com.atomgraph.linkeddatahub.cli.command.Delete; +import com.atomgraph.linkeddatahub.cli.command.Get; +import com.atomgraph.linkeddatahub.cli.command.Patch; +import com.atomgraph.linkeddatahub.cli.command.Post; +import com.atomgraph.linkeddatahub.cli.command.Put; +import com.atomgraph.linkeddatahub.cli.command.admin.Admin; +import com.atomgraph.linkeddatahub.cli.command.content.Content; +import com.atomgraph.linkeddatahub.cli.command.imports.Imports; +import org.apache.jena.sys.JenaSystem; +import picocli.AutoComplete; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ScopeType; + +/** + * Root command of the LinkedDataHub CLI. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "ldh", + mixinStandardHelpOptions = true, + version = "ldh 1.0.0-SNAPSHOT", + description = "Command line interface for the LinkedDataHub HTTP API.", + subcommands = { + Get.class, Post.class, Put.class, Patch.class, Delete.class, + CreateItem.class, CreateContainer.class, + AddView.class, AddConstruct.class, AddSelect.class, AddResultSetChart.class, AddFile.class, AddGenericService.class, + Admin.class, Content.class, Imports.class, + AutoComplete.GenerateCompletion.class + }) +public class LDH +{ + + @Option(names = "--verbose", scope = ScopeType.INHERIT, description = "Print stack traces of errors") + boolean verbose; + + /** + * CLI entry point. + * + * @param args command line arguments + */ + public static void main(String[] args) + { + JenaSystem.init(); + + CommandLine cmd = new CommandLine(new LDH()); + cmd.setExecutionExceptionHandler(LDH::handleExecutionException); + System.exit(cmd.execute(args)); + } + + static int handleExecutionException(Exception ex, CommandLine cmdLine, CommandLine.ParseResult parseResult) + { + boolean verbose = cmdLine.getCommandSpec().root().userObject() instanceof LDH root && root.verbose; + + cmdLine.getErr().println(cmdLine.getColorScheme().errorText(messageOf(ex))); + if (verbose) ex.printStackTrace(cmdLine.getErr()); + + return CommandLine.ExitCode.SOFTWARE; + } + + static String messageOf(Throwable ex) + { + // unwrap Jersey ProcessingException chains down to the I/O cause, e.g. "Connection refused" + Throwable cause = ex; + while (cause.getCause() != null && (cause instanceof jakarta.ws.rs.ProcessingException || cause.getMessage() == null)) + cause = cause.getCause(); + + return cause.getMessage() != null ? cause.getMessage() : cause.toString(); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java new file mode 100644 index 000000000..91194f7b2 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java @@ -0,0 +1,120 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL CONSTRUCT query to a document. Mirrors bin/add-construct.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-construct", description = "Adds a CONSTRUCT query to a document.") +public class AddConstruct extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the query") + private String title; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the query (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--service", paramLabel = "SERVICE_URI", description = "URI of the SPARQL service (optional)") + private URI service; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + core(getClient(), target, uri, title, Files.readString(queryFile), service, description); + print(target); + + return 0; + } + + /** + * Appends the CONSTRUCT query description to the target document. + * + * @param client client instance + * @param target target document URI + * @param uri query URI (optional) + * @param title query title + * @param queryText query string + * @param service SPARQL service URI (optional) + * @param description query description (optional) + */ + public static void core(LDHClient client, URI target, String uri, String title, String queryText, URI service, String description) + { + post(client, target, buildModel(target, uri, SP.Construct, title, queryText, service, description)); + } + + /** + * Builds a SPIN query description. + * + * @param target target document URI + * @param uri query URI (optional) + * @param queryType SPIN query class (sp:Construct or sp:Select) + * @param title query title + * @param queryText query string + * @param service SPARQL service URI (optional) + * @param description query description (optional) + * @return query model + */ + public static Model buildModel(URI target, String uri, Resource queryType, String title, String queryText, URI service, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource query = createSubject(model, target, uri). + addProperty(RDF.type, queryType). + addProperty(DCTerms.title, title). + addProperty(SP.text, queryText); + if (service != null) query.addProperty(LDH.service, model.createResource(service.toString())); + if (description != null) query.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java new file mode 100644 index 000000000..8114dec2a --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java @@ -0,0 +1,141 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Digests; +import com.atomgraph.linkeddatahub.cli.vocab.NFO; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Uploads a file using the RDF/POST multipart encoding. Mirrors bin/add-file.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-file", description = "Uploads a file.") +public class AddFile extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the file") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the file (optional)") + private String description; + + @Option(names = "--file", required = true, paramLabel = "ABS_PATH", description = "Path to the file") + private Path file; + + @Option(names = "--content-type", paramLabel = "MEDIA_TYPE", description = "Media type of the file (optional, auto-detected if not set)") + private String contentType; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + + URI fileURI = core(getClient(), base, target, file, contentType, title, description); + print(fileURI); + + return 0; + } + + /** + * Uploads the file to the target document and returns its content-addressed upload URI. + * The RDF/POST field order is positional: each pu must immediately precede + * its ol/ou value. + * + * @param client client instance + * @param base application base URI + * @param target target document URI + * @param file file path + * @param contentType file media type (optional, auto-detected if null) + * @param title file title + * @param description file description (optional) + * @return upload URI derived from the SHA1 hash of the file content + * @throws IOException file read error + */ + public static URI core(LDHClient client, URI base, URI target, Path file, String contentType, String title, String description) throws IOException + { + String fileContentType = contentType != null ? contentType : detectContentType(file); + + try (FormDataMultiPart multiPart = buildMultiPart(file, fileContentType, title, description)) + { + HttpException.check(target, client.post(target, Entity.entity(multiPart, multiPart.getMediaType()), ACCEPT_TURTLE)).close(); + } + + return URI.create(base.toString() + "uploads/" + Digests.sha1Hex(file)); + } + + /** + * Builds the RDF/POST multipart body. + * + * @param file file path + * @param contentType file media type + * @param title file title + * @param description file description (optional) + * @return multipart body + */ + public static FormDataMultiPart buildMultiPart(Path file, String contentType, String title, String description) + { + FormDataMultiPart multiPart = new FormDataMultiPart(); + + multiPart.field("rdf", ""); + multiPart.field("sb", "file"); + multiPart.field("pu", NFO.fileName.getURI()); + multiPart.bodyPart(new FileDataBodyPart("ol", file.toFile(), MediaType.valueOf(contentType))); + multiPart.field("pu", DCTerms.title.getURI()); + multiPart.field("ol", title); + multiPart.field("pu", RDF.type.getURI()); + multiPart.field("ou", NFO.FileDataObject.getURI()); + if (description != null) + { + multiPart.field("pu", DCTerms.description.getURI()); + multiPart.field("ol", description); + } + + return multiPart; + } + + static String detectContentType(Path file) throws IOException + { + String detected = Files.probeContentType(file); + return detected != null ? detected : "application/octet-stream"; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java new file mode 100644 index 000000000..cb737c570 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java @@ -0,0 +1,112 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.A; +import com.atomgraph.linkeddatahub.cli.vocab.SD; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends a generic SPARQL service description to a document. Mirrors bin/add-generic-service.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-generic-service", description = "Appends a generic SPARQL service to a document.") +public class AddGenericService extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the service") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the service (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the service (optional, blank node if not set)") + private String uri; + + @Option(names = "--endpoint", required = true, paramLabel = "ENDPOINT_URI", description = "URI of the SPARQL endpoint") + private URI endpoint; + + @Option(names = "--graph-store", paramLabel = "GRAPH_STORE_URI", description = "URI of the Graph Store Protocol endpoint (optional)") + private URI graphStore; + + @Option(names = "--auth-user", paramLabel = "AUTH_USER", description = "Username for HTTP Basic auth (optional)") + private String authUser; + + @Option(names = "--auth-pwd", paramLabel = "AUTH_PASSWORD", description = "Password for HTTP Basic auth (optional)") + private String authPwd; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, title, endpoint, graphStore, authUser, authPwd, description)); + print(target); + + return 0; + } + + /** + * Builds the service description. + * + * @param target target document URI + * @param uri service URI (optional) + * @param title service title + * @param endpoint SPARQL endpoint URI + * @param graphStore Graph Store Protocol endpoint URI (optional) + * @param authUser HTTP Basic auth username (optional) + * @param authPwd HTTP Basic auth password (optional) + * @param description service description (optional) + * @return service model + */ + public static Model buildModel(URI target, String uri, String title, URI endpoint, URI graphStore, String authUser, String authPwd, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource service = createSubject(model, target, uri). + addProperty(RDF.type, SD.Service). + addProperty(DCTerms.title, title). + addProperty(SD.endpoint, model.createResource(endpoint.toString())). + addProperty(SD.supportedLanguage, SD.SPARQL11Query). + addProperty(SD.supportedLanguage, SD.SPARQL11Update); + if (graphStore != null) service.addProperty(A.graphStore, model.createResource(graphStore.toString())); + if (authUser != null) service.addProperty(A.authUser, authUser); + if (authPwd != null) service.addProperty(A.authPwd, authPwd); + if (description != null) service.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java new file mode 100644 index 000000000..9b1555382 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java @@ -0,0 +1,110 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends a chart of SPARQL SELECT results to a document. Mirrors bin/add-result-set-chart.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-result-set-chart", description = "Appends a result set chart to a document.") +public class AddResultSetChart extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the chart") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the chart (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the chart (optional, blank node if not set)") + private String uri; + + @Option(names = "--query", required = true, paramLabel = "QUERY_URI", description = "URI of the SELECT query") + private URI query; + + @Option(names = "--chart-type", required = true, paramLabel = "TYPE_URI", description = "URI of the chart type") + private URI chartType; + + @Option(names = "--category-var-name", required = true, paramLabel = "VAR_NAME", description = "Name of the category variable") + private String categoryVarName; + + @Option(names = "--series-var-name", required = true, paramLabel = "VAR_NAME", description = "Name of the series variable") + private String seriesVarName; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, title, query, chartType, categoryVarName, seriesVarName, description)); + print(target); + + return 0; + } + + /** + * Builds the chart description. + * + * @param target target document URI + * @param uri chart URI (optional) + * @param title chart title + * @param query SELECT query URI + * @param chartType chart type URI + * @param categoryVarName category variable name + * @param seriesVarName series variable name + * @param description chart description (optional) + * @return chart model + */ + public static Model buildModel(URI target, String uri, String title, URI query, URI chartType, String categoryVarName, String seriesVarName, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource chart = createSubject(model, target, uri). + addProperty(RDF.type, LDH.ResultSetChart). + addProperty(DCTerms.title, title). + addProperty(SPIN.query, model.createResource(query.toString())). + addProperty(LDH.chartType, model.createResource(chartType.toString())). + addProperty(LDH.categoryVarName, categoryVarName). + addProperty(LDH.seriesVarName, seriesVarName); + if (description != null) chart.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java new file mode 100644 index 000000000..63698b342 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java @@ -0,0 +1,71 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL SELECT query to a document. Mirrors bin/add-select.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-select", description = "Adds a SELECT query to a document.") +public class AddSelect extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the query") + private String title; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the query (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--service", paramLabel = "SERVICE_URI", description = "URI of the SPARQL service (optional)") + private URI service; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, AddConstruct.buildModel(target, uri, SP.Select, title, Files.readString(queryFile), service, description)); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java new file mode 100644 index 000000000..345775f45 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java @@ -0,0 +1,101 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.AC; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends a view of a SPARQL SELECT query to a document. Mirrors bin/add-view.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-view", description = "Appends a view to a document.") +public class AddView extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--query", required = true, paramLabel = "QUERY_URI", description = "URI of the SELECT query") + private URI query; + + @Option(names = "--title", paramLabel = "TITLE", description = "Title of the view (optional)") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the view (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the view (optional, blank node if not set)") + private String uri; + + @Option(names = "--mode", paramLabel = "MODE_URI", description = "URI of the layout mode (optional)") + private URI mode; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, query, title, description, mode)); + print(target); + + return 0; + } + + /** + * Builds the view description. + * + * @param target target document URI + * @param uri view URI (optional) + * @param query SELECT query URI + * @param title view title (optional) + * @param description view description (optional) + * @param mode layout mode URI (optional) + * @return view model + */ + public static Model buildModel(URI target, String uri, URI query, String title, String description, URI mode) + { + Model model = ModelFactory.createDefaultModel(); + + Resource view = createSubject(model, target, uri). + addProperty(RDF.type, LDH.View). + addProperty(SPIN.query, model.createResource(query.toString())); + if (title != null) view.addProperty(DCTerms.title, title); + if (description != null) view.addProperty(DCTerms.description, description); + if (mode != null) view.addProperty(AC.mode, model.createResource(mode.toString())); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java new file mode 100644 index 000000000..5e3e327e1 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java @@ -0,0 +1,114 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.AC; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates a container document. Mirrors bin/create-container.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-container", description = "Creates a container document.") +public class CreateContainer extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the container") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the container (optional)") + private String description; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--parent", required = true, paramLabel = "PARENT_URI", description = "URI of the parent container") + private URI parent; + + @Option(names = "--block", paramLabel = "BLOCK_URI", description = "URI of the content block (optional)") + private URI block; + + @Option(names = "--mode", paramLabel = "MODE_URI", description = "URI of the layout mode of the children view (optional)") + private URI mode; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + URI doc = URIRewriter.childURI(parent, slug != null ? slug : Slugs.defaultSlug()); + put(getClient(), doc, buildModel(doc, title, description, block, mode)); + print(doc); + + return 0; + } + + /** + * Builds the container document model with its first content block: the given block URI, + * a children view with an explicit mode, or the default children view. + * + * @param doc document URI + * @param title document title + * @param description document description (optional) + * @param block content block URI (optional) + * @param mode children view mode URI (optional, ignored when block is given) + * @return document model + */ + public static Model buildModel(URI doc, String title, String description, URI block, URI mode) + { + Model model = ModelFactory.createDefaultModel(); + + Resource container = model.createResource(doc.toString()). + addProperty(RDF.type, DH.Container). + addProperty(DCTerms.title, title); + + if (block != null) container.addProperty(RDF.li(1), model.createResource(block.toString())); + else if (mode != null) container.addProperty(RDF.li(1), model.createResource(). + addProperty(RDF.type, LDH.Object). + addProperty(RDF.value, model.createResource(). + addProperty(RDF.type, LDH.View). + addProperty(SPIN.query, LDH.SelectChildren). + addProperty(AC.mode, model.createResource(mode.toString())))); + else container.addProperty(RDF.li(1), model.createResource(). + addProperty(RDF.type, LDH.Object). + addProperty(RDF.value, LDH.ChildrenView)); + + if (description != null) container.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java new file mode 100644 index 000000000..6f2e49b83 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java @@ -0,0 +1,90 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates an item document. Mirrors bin/create-item.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-item", description = "Creates an item document.") +public class CreateItem extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the item") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the item (optional)") + private String description; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--container", required = true, paramLabel = "CONTAINER_URI", description = "URI of the parent container") + private URI container; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + URI doc = URIRewriter.childURI(container, slug != null ? slug : Slugs.defaultSlug()); + put(getClient(), doc, buildModel(doc, title, description)); + print(doc); + + return 0; + } + + /** + * Builds the item document model. + * + * @param doc document URI + * @param title document title + * @param description document description (optional) + * @return document model + */ + public static Model buildModel(URI doc, String title, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource item = model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(DCTerms.title, title); + if (description != null) item.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java new file mode 100644 index 000000000..27988751c --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java @@ -0,0 +1,45 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + +/** + * Deletes an RDF document. Mirrors bin/delete.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "delete", description = "Deletes an RDF document.") +public class Delete extends BaseCommand +{ + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().delete(target)).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java new file mode 100644 index 000000000..2244ad231 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java @@ -0,0 +1,63 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Retrieves an RDF description. Mirrors bin/get.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "get", description = "Retrieves RDF description.") +public class Get extends BaseCommand +{ + + @Option(names = "--accept", required = true, paramLabel = "MEDIA_TYPE", description = "Requested media type (e.g. text/turtle)") + private String accept; + + @Option(names = "--head", description = "Requested headers only, no body (HEAD method)") + private boolean head; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + MediaType[] acceptedTypes = { MediaType.valueOf(accept) }; + + if (head) + try (Response response = HttpException.check(target, getClient().head(target, acceptedTypes))) + { + print("HTTP " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); + response.getStringHeaders().forEach((name, values) -> values.forEach(value -> print(name + ": " + value))); + } + else + printBody(HttpException.check(target, getClient().get(target, acceptedTypes))); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java new file mode 100644 index 000000000..f41a9deed --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java @@ -0,0 +1,52 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import org.apache.jena.query.Syntax; +import org.apache.jena.update.UpdateFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + +/** + * Patches an RDF document with a SPARQL update from standard input. Mirrors bin/patch.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "patch", description = "Patches an RDF document using SPARQL update from stdin.") +public class Patch extends BaseCommand +{ + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + String update = new String(System.in.readAllBytes(), StandardCharsets.UTF_8); + // validate as standard SPARQL 1.1 before sending; the original text is sent unmodified + UpdateFactory.create(update, target.toString(), Syntax.syntaxSPARQL_11); + + HttpException.check(target, getClient().patch(target, update)).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java new file mode 100644 index 000000000..8cc1aa393 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java @@ -0,0 +1,54 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Creates an RDF document from standard input. Mirrors bin/post.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "post", description = "Creates an RDF document from stdin.") +public class Post extends BaseCommand +{ + + @Option(names = {"-t", "--content-type"}, required = true, paramLabel = "MEDIA_TYPE", description = "Media type of the RDF body (e.g. text/turtle)") + private String contentType; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + Model model = readModel(contentType, target, System.in); + HttpException.check(target, getClient().post(target, Entity.entity(model, MediaType.valueOf(contentType)), ACCEPT_TURTLE)).close(); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java new file mode 100644 index 000000000..d30706740 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java @@ -0,0 +1,54 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Creates or updates an RDF document from standard input. Mirrors bin/put.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "put", description = "Creates or updates an RDF document from stdin.") +public class Put extends BaseCommand +{ + + @Option(names = {"-t", "--content-type"}, required = true, paramLabel = "MEDIA_TYPE", description = "Media type of the RDF body (e.g. text/turtle)") + private String contentType; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + Model model = readModel(contentType, target, System.in); + HttpException.check(target, getClient().put(target, Entity.entity(model, MediaType.valueOf(contentType)), ACCEPT_TURTLE)).close(); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java new file mode 100644 index 000000000..5a3ffc149 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java @@ -0,0 +1,50 @@ +/* + * 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.cli.command.admin; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds an owl:imports statement to an ontology. Mirrors bin/admin/add-ontology-import.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-ontology-import", description = "Adds an owl:imports statement to an ontology.") +public class AddOntologyImport extends BaseCommand +{ + + @Option(names = "--import", required = true, paramLabel = "IMPORT_URI", description = "URI of the imported ontology") + private URI importURI; + + @Parameters(paramLabel = "ONTOLOGY_DOC_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().patch(target, Updates.insertOntologyImport(target, importURI))).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java new file mode 100644 index 000000000..f2be93bdf --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java @@ -0,0 +1,48 @@ +/* + * 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.cli.command.admin; + +import com.atomgraph.linkeddatahub.cli.command.admin.acl.Acl; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.Ontologies; +import com.atomgraph.linkeddatahub.cli.command.admin.packages.Packages; +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Administrative command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "admin", + description = "Administrative commands.", + subcommands = { Ontologies.class, Acl.class, Packages.class, ClearOntology.class, AddOntologyImport.class }) +public class Admin implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java new file mode 100644 index 000000000..642451bb3 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java @@ -0,0 +1,55 @@ +/* + * 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.cli.command.admin; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import jakarta.ws.rs.core.Form; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Clears an ontology from memory so it gets reloaded. Mirrors bin/admin/clear-ontology.sh. + * The base URI is the base of the admin application. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "clear-ontology", description = "Clears an ontology from memory and reloads it.") +public class ClearOntology extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--ontology", required = true, paramLabel = "ONTOLOGY_URI", description = "URI of the ontology") + private URI ontology; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI target = URI.create(base + "clear"); + + printBody(HttpException.check(target, getClient().postForm(target, new Form("uri", ontology.toString()), ACCEPT_TURTLE))); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java new file mode 100644 index 000000000..c44ef168d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java @@ -0,0 +1,45 @@ +/* + * 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.cli.command.admin.acl; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Access control command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "acl", + description = "Access control commands.", + subcommands = { CreateGroup.class, CreateAuthorization.class, AddAgentToGroup.class, MakePublic.class }) +public class Acl implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java new file mode 100644 index 000000000..73abb82ff --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java @@ -0,0 +1,50 @@ +/* + * 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.cli.command.admin.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds an agent to a group. Mirrors bin/admin/acl/add-agent-to-group.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-agent-to-group", description = "Adds an agent to a group.") +public class AddAgentToGroup extends BaseCommand +{ + + @Option(names = "--agent", required = true, paramLabel = "AGENT_URI", description = "URI of the agent") + private URI agent; + + @Parameters(paramLabel = "GROUP_DOC_URI", description = "URI of the group document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().patch(target, Updates.insertGroupMember(target, agent))).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java new file mode 100644 index 000000000..2c43b8ba4 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java @@ -0,0 +1,163 @@ +/* + * 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.cli.command.admin.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.ACL; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * Creates an ACL authorization. Mirrors bin/admin/acl/create-authorization.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-authorization", description = "Creates an ACL authorization.") +public class CreateAuthorization extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the authorization") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the authorization (optional)") + private String comment; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the authorization (optional, blank node if not set)") + private String uri; + + @Option(names = "--agent", paramLabel = "AGENT_URI", description = "URI of an authorized agent (repeatable)") + private List agents = new ArrayList<>(); + + @Option(names = "--agent-class", paramLabel = "AGENT_CLASS_URI", description = "URI of an authorized agent class (repeatable)") + private List agentClasses = new ArrayList<>(); + + @Option(names = "--agent-group", paramLabel = "AGENT_GROUP_URI", description = "URI of an authorized agent group (repeatable)") + private List agentGroups = new ArrayList<>(); + + @Option(names = "--to", paramLabel = "TO_URI", description = "URI of an accessed document (repeatable)") + private List to = new ArrayList<>(); + + @Option(names = "--to-all-in", paramLabel = "CLASS_URI", description = "URI of an accessed document class (repeatable)") + private List toAllIn = new ArrayList<>(); + + @Option(names = "--append", description = "Grant acl:Append mode") + private boolean append; + + @Option(names = "--control", description = "Grant acl:Control mode") + private boolean control; + + @Option(names = "--read", description = "Grant acl:Read mode") + private boolean read; + + @Option(names = "--write", description = "Grant acl:Write mode") + private boolean write; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + if (agents.isEmpty() && agentClasses.isEmpty() && agentGroups.isEmpty()) + throw new ParameterException(getSpec().commandLine(), "At least one of '--agent', '--agent-class', '--agent-group' is required"); + if (to.isEmpty() && toAllIn.isEmpty()) + throw new ParameterException(getSpec().commandLine(), "At least one of '--to', '--to-all-in' is required"); + if (!append && !control && !read && !write) + throw new ParameterException(getSpec().commandLine(), "At least one of '--append', '--control', '--read', '--write' is required"); + + URI doc = URIRewriter.childURI(URI.create(base + "acl/authorizations/"), slug != null ? slug : Slugs.defaultSlug()); + + List modes = new ArrayList<>(); + if (append) modes.add(ACL.Append); + if (control) modes.add(ACL.Control); + if (read) modes.add(ACL.Read); + if (write) modes.add(ACL.Write); + + put(getClient(), doc, buildModel(doc, uri, label, comment, agents, agentClasses, agentGroups, to, toAllIn, modes)); + print(doc); + + return 0; + } + + /** + * Builds the authorization document model. + * + * @param doc document URI + * @param uri authorization URI (optional, blank node if null) + * @param label authorization label + * @param comment authorization comment (optional) + * @param agents authorized agent URIs + * @param agentClasses authorized agent class URIs + * @param agentGroups authorized agent group URIs + * @param to accessed document URIs + * @param toAllIn accessed document class URIs + * @param modes granted access modes + * @return document model + */ + public static Model buildModel(URI doc, String uri, String label, String comment, + List agents, List agentClasses, List agentGroups, + List to, List toAllIn, List modes) + { + Model model = ModelFactory.createDefaultModel(); + + Resource auth = createSubject(model, doc, uri). + addProperty(RDF.type, ACL.Authorization). + addProperty(RDFS.label, label); + if (comment != null) auth.addProperty(RDFS.comment, comment); + + model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(FOAF.primaryTopic, auth). + addProperty(DCTerms.title, label); + + addResourceValues(auth, ACL.agent, agents); + addResourceValues(auth, ACL.agentClass, agentClasses); + addResourceValues(auth, ACL.agentGroup, agentGroups); + addResourceValues(auth, ACL.accessTo, to); + addResourceValues(auth, ACL.accessToClass, toAllIn); + modes.forEach(mode -> auth.addProperty(ACL.mode, mode)); + + return model; + } + + static void addResourceValues(Resource subject, Property property, List values) + { + values.forEach(value -> subject.addProperty(property, subject.getModel().createResource(value.toString()))); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java new file mode 100644 index 000000000..05ec6847e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java @@ -0,0 +1,103 @@ +/* + * 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.cli.command.admin.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates an agent group. Mirrors bin/admin/acl/create-group.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-group", description = "Creates an agent group.") +public class CreateGroup extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--name", required = true, paramLabel = "NAME", description = "Name of the group") + private String name; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the group (optional)") + private String description; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the group (optional, blank node if not set)") + private String uri; + + @Option(names = "--member", required = true, paramLabel = "MEMBER_URI", description = "URI of a group member (repeatable)") + private List members; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI doc = URIRewriter.childURI(URI.create(base + "acl/groups/"), slug != null ? slug : Slugs.defaultSlug()); + + put(getClient(), doc, buildModel(doc, uri, name, description, members)); + print(doc); + + return 0; + } + + /** + * Builds the group document model. + * + * @param doc document URI + * @param uri group URI (optional, blank node if null) + * @param name group name + * @param description group description (optional) + * @param members member agent URIs + * @return document model + */ + public static Model buildModel(URI doc, String uri, String name, String description, List members) + { + Model model = ModelFactory.createDefaultModel(); + + Resource group = createSubject(model, doc, uri). + addProperty(RDF.type, FOAF.Group). + addProperty(FOAF.name, name); + if (description != null) group.addProperty(DCTerms.description, description); + members.forEach(member -> group.addProperty(FOAF.member, model.createResource(member.toString()))); + + model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(FOAF.primaryTopic, group). + addProperty(DCTerms.title, name); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java new file mode 100644 index 000000000..a041e3521 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java @@ -0,0 +1,59 @@ +/* + * 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.cli.command.admin.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +/** + * Makes all end-user application documents publicly readable. Mirrors bin/admin/acl/make-public.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "make-public", description = "Makes all end-user application documents publicly readable.") +public class MakePublic extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI adminBase = URIRewriter.adminBase(base); + URI target = URI.create(adminBase + "acl/authorizations/public/"); + + HttpException.check(target, getClient().patch(target, Updates.makePublic(base, adminBase))).close(); + + return 0; + } + + @Override + protected URI getEffectiveProxy() + { + // the request targets the admin app, so the proxy origin gets the admin subdomain too + return getProxyMixin().getProxy() != null ? URIRewriter.adminBase(getProxyMixin().getProxy()) : null; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java new file mode 100644 index 000000000..64c097ce2 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java @@ -0,0 +1,107 @@ +/* + * 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.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a class to an ontology. Mirrors bin/admin/ontologies/add-class.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-class", description = "Adds a class to an ontology.") +public class AddClass extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the class") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the class (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the class (optional, blank node if not set)") + private String uri; + + @Option(names = "--constructor", paramLabel = "CONSTRUCT_URI", description = "URI of the constructor query (optional)") + private URI constructor; + + @Option(names = "--constraint", paramLabel = "CONSTRAINT_URI", description = "URI of the constraint (optional)") + private URI constraint; + + @Option(names = "--sub-class-of", paramLabel = "SUPER_CLASS_URI", description = "URI of a superclass (optional, repeatable)") + private List superClasses = new ArrayList<>(); + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, label, comment, constructor, constraint, superClasses)); + print(target); + + return 0; + } + + /** + * Builds the class description. + * + * @param target target document URI + * @param uri class URI (optional) + * @param label class label + * @param comment class comment (optional) + * @param constructor constructor query URI (optional) + * @param constraint constraint URI (optional) + * @param superClasses superclass URIs + * @return class model + */ + public static Model buildModel(URI target, String uri, String label, String comment, URI constructor, URI constraint, List superClasses) + { + Model model = ModelFactory.createDefaultModel(); + + Resource cls = createSubject(model, target, uri). + addProperty(RDF.type, OWL.Class). + addProperty(RDFS.label, label); + if (comment != null) cls.addProperty(RDFS.comment, comment); + if (constructor != null) cls.addProperty(SPIN.constructor, model.createResource(constructor.toString())); + if (constraint != null) cls.addProperty(SPIN.constraint, model.createResource(constraint.toString())); + superClasses.forEach(superClass -> cls.addProperty(RDFS.subClassOf, model.createResource(superClass.toString()))); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java new file mode 100644 index 000000000..d5f222b64 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java @@ -0,0 +1,101 @@ +/* + * 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.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL CONSTRUCT constructor query to an ontology. Mirrors bin/admin/ontologies/add-constructor.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-constructor", description = "Adds a CONSTRUCT query to an ontology.") +public class AddConstructor extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the query") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the query (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, SP.Construct, label, Files.readString(queryFile), null, comment)); + print(target); + + return 0; + } + + /** + * Builds an ontology SPIN query description (labeled with rdfs:label, + * unlike the dct:title-based document queries). + * + * @param target target document URI + * @param uri query URI (optional) + * @param queryType SPIN query class (sp:Construct or sp:Select) + * @param label query label + * @param queryText query string + * @param service SPARQL service URI (optional) + * @param comment query comment (optional) + * @return query model + */ + public static Model buildModel(URI target, String uri, Resource queryType, String label, String queryText, URI service, String comment) + { + Model model = ModelFactory.createDefaultModel(); + + Resource query = createSubject(model, target, uri). + addProperty(RDF.type, queryType). + addProperty(RDFS.label, label). + addProperty(SP.text, queryText); + if (comment != null) query.addProperty(RDFS.comment, comment); + if (service != null) query.addProperty(LDH.service, model.createResource(service.toString())); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java new file mode 100644 index 000000000..74ff2db86 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java @@ -0,0 +1,95 @@ +/* + * 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.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a constraint that makes a property required. Mirrors bin/admin/ontologies/add-property-constraint.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-property-constraint", description = "Adds a constraint that makes a property required.") +public class AddPropertyConstraint extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the constraint") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the constraint (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the constraint (optional, blank node if not set)") + private String uri; + + @Option(names = "--property", required = true, paramLabel = "PROPERTY_URI", description = "URI of the required property") + private URI property; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, label, property, comment)); + print(target); + + return 0; + } + + /** + * Builds the constraint description. + * + * @param target target document URI + * @param uri constraint URI (optional) + * @param label constraint label + * @param property required property URI + * @param comment constraint comment (optional) + * @return constraint model + */ + public static Model buildModel(URI target, String uri, String label, URI property, String comment) + { + Model model = ModelFactory.createDefaultModel(); + + Resource constraint = createSubject(model, target, uri). + addProperty(RDF.type, LDH.MissingPropertyValue). + addProperty(RDFS.label, label). + addProperty(SP.arg1, model.createResource(property.toString())); + if (comment != null) constraint.addProperty(RDFS.comment, comment); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java new file mode 100644 index 000000000..c2b6ee6ce --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java @@ -0,0 +1,104 @@ +/* + * 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.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds an OWL restriction to an ontology. Mirrors bin/admin/ontologies/add-restriction.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-restriction", description = "Adds an OWL restriction to an ontology.") +public class AddRestriction extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the restriction") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the restriction (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the restriction (optional, blank node if not set)") + private String uri; + + @Option(names = "--on-property", paramLabel = "PROPERTY_URI", description = "URI of the restricted property (optional)") + private URI onProperty; + + @Option(names = "--all-values-from", paramLabel = "URI", description = "URI of the value class (optional)") + private URI allValuesFrom; + + @Option(names = "--has-value", paramLabel = "URI", description = "URI of the value resource (optional)") + private URI hasValue; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, label, comment, onProperty, allValuesFrom, hasValue)); + print(target); + + return 0; + } + + /** + * Builds the restriction description. + * + * @param target target document URI + * @param uri restriction URI (optional) + * @param label restriction label + * @param comment restriction comment (optional) + * @param onProperty restricted property URI (optional) + * @param allValuesFrom value class URI (optional) + * @param hasValue value resource URI (optional) + * @return restriction model + */ + public static Model buildModel(URI target, String uri, String label, String comment, URI onProperty, URI allValuesFrom, URI hasValue) + { + Model model = ModelFactory.createDefaultModel(); + + Resource restriction = createSubject(model, target, uri). + addProperty(RDF.type, OWL.Restriction). + addProperty(RDFS.label, label); + if (comment != null) restriction.addProperty(RDFS.comment, comment); + if (onProperty != null) restriction.addProperty(OWL.onProperty, model.createResource(onProperty.toString())); + if (allValuesFrom != null) restriction.addProperty(OWL.allValuesFrom, model.createResource(allValuesFrom.toString())); + if (hasValue != null) restriction.addProperty(OWL.hasValue, model.createResource(hasValue.toString())); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java new file mode 100644 index 000000000..0938ffc9e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java @@ -0,0 +1,71 @@ +/* + * 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.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL SELECT query to an ontology. Mirrors bin/admin/ontologies/add-select.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-select", description = "Adds a SELECT query to an ontology.") +public class AddSelect extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the query") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the query (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Option(names = "--service", paramLabel = "SERVICE_URI", description = "URI of the SPARQL service (optional)") + private URI service; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, AddConstructor.buildModel(target, uri, SP.Select, label, Files.readString(queryFile), service, comment)); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java new file mode 100644 index 000000000..96cface9f --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates a new ontology document. Mirrors bin/admin/ontologies/create-ontology.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-ontology", description = "Creates a new ontology.") +public class CreateOntology extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the ontology") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the ontology (optional)") + private String comment; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the ontology (optional, blank node if not set)") + private String uri; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI doc = URIRewriter.childURI(URI.create(base + "ontologies/"), slug != null ? slug : Slugs.defaultSlug()); + + put(getClient(), doc, buildModel(doc, uri, label, comment)); + print(doc); + + return 0; + } + + /** + * Builds the ontology document model. + * + * @param doc document URI + * @param uri ontology URI (optional, blank node if null) + * @param label ontology label + * @param comment ontology comment (optional) + * @return document model + */ + public static Model buildModel(URI doc, String uri, String label, String comment) + { + Model model = ModelFactory.createDefaultModel(); + + Resource ontology = createSubject(model, doc, uri). + addProperty(RDF.type, OWL.Ontology). + addProperty(RDFS.label, label); + if (comment != null) ontology.addProperty(RDFS.comment, comment); + + model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(FOAF.primaryTopic, ontology). + addProperty(DCTerms.title, label); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java new file mode 100644 index 000000000..a91607456 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java @@ -0,0 +1,83 @@ +/* + * 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.cli.command.admin.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SD; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import jakarta.ws.rs.client.Entity; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.vocabulary.DCTerms; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Imports an external ontology into a named graph via the transform endpoint. + * Mirrors bin/admin/ontologies/import-ontology.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import-ontology", description = "Imports an external ontology into a named graph.") +public class ImportOntology extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--source", required = true, paramLabel = "SOURCE_URI", description = "URI of the imported ontology") + private URI source; + + @Option(names = "--graph", required = true, paramLabel = "GRAPH_URI", description = "URI of the named graph the ontology is imported into") + private URI graph; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI target = URI.create(base + "transform"); + + printBody(HttpException.check(target, getClient().post(target, Entity.entity(buildModel(base, source, graph), TEXT_TURTLE_TYPE), ACCEPT_TURTLE))); + + return 0; + } + + /** + * Builds the transform argument description. + * + * @param base admin application base URI + * @param source imported ontology URI + * @param graph target named graph URI + * @return argument model + */ + public static Model buildModel(URI base, URI source, URI graph) + { + Model model = ModelFactory.createDefaultModel(); + + model.createResource(). + addProperty(DCTerms.source, model.createResource(source.toString())). + addProperty(SD.name, model.createResource(graph.toString())). + addProperty(SPIN.query, model.createResource(base + "queries/construct-constructors/#this")); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java new file mode 100644 index 000000000..e7fdbdec4 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java @@ -0,0 +1,46 @@ +/* + * 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.cli.command.admin.ontologies; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Ontology management command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "ontologies", + description = "Ontology management commands.", + subcommands = { CreateOntology.class, ImportOntology.class, AddClass.class, AddConstructor.class, + AddSelect.class, AddPropertyConstraint.class, AddRestriction.class }) +public class Ontologies implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java new file mode 100644 index 000000000..f2167060d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java @@ -0,0 +1,60 @@ +/* + * 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.cli.command.admin.packages; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import jakarta.ws.rs.core.Form; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Installs a LinkedDataHub package. Mirrors bin/admin/packages/install-package.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "install-package", description = "Installs a package.") +public class InstallPackage extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--package", required = true, paramLabel = "PACKAGE_URI", description = "URI of the package") + private URI packageURI; + + @Override + public Integer call() throws Exception + { + URI target = URI.create(URIRewriter.adminBase(baseMixin.require(getSpec())) + "packages/install"); + HttpException.check(target, getClient().postForm(target, new Form("package-uri", packageURI.toString()), ACCEPT_TURTLE)).close(); + + return 0; + } + + @Override + protected URI getEffectiveProxy() + { + // the request targets the admin app, so the proxy origin gets the admin subdomain too + return getProxyMixin().getProxy() != null ? URIRewriter.adminBase(getProxyMixin().getProxy()) : null; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java new file mode 100644 index 000000000..79191eb13 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java @@ -0,0 +1,45 @@ +/* + * 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.cli.command.admin.packages; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Package management command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "packages", + description = "Package management commands.", + subcommands = { InstallPackage.class, UninstallPackage.class }) +public class Packages implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java new file mode 100644 index 000000000..78f463673 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java @@ -0,0 +1,60 @@ +/* + * 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.cli.command.admin.packages; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import jakarta.ws.rs.core.Form; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Uninstalls a LinkedDataHub package. Mirrors bin/admin/packages/uninstall-package.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "uninstall-package", description = "Uninstalls a package.") +public class UninstallPackage extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--package", required = true, paramLabel = "PACKAGE_URI", description = "URI of the package") + private URI packageURI; + + @Override + public Integer call() throws Exception + { + URI target = URI.create(URIRewriter.adminBase(baseMixin.require(getSpec())) + "packages/uninstall"); + HttpException.check(target, getClient().postForm(target, new Form("package-uri", packageURI.toString()), ACCEPT_TURTLE)).close(); + + return 0; + } + + @Override + protected URI getEffectiveProxy() + { + // the request targets the admin app, so the proxy origin gets the admin subdomain too + return getProxyMixin().getProxy() != null ? URIRewriter.adminBase(getProxyMixin().getProxy()) : null; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java new file mode 100644 index 000000000..b9860d23d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java @@ -0,0 +1,121 @@ +/* + * 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.cli.command.content; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.SequenceNumbers; +import com.atomgraph.linkeddatahub.cli.vocab.AC; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends an object content block to a document. Mirrors bin/content/add-object-block.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-object-block", description = "Appends an object content block to a document.") +public class AddObjectBlock extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; // accepted for script interface parity, unused + + @Option(names = "--value", required = true, paramLabel = "RESOURCE_URI", description = "URI of the object resource") + private URI value; + + @Option(names = "--title", paramLabel = "TITLE", description = "Title of the block (optional)") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the block (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the block (optional, blank node if not set)") + private String uri; + + @Option(names = "--mode", paramLabel = "MODE_URI", description = "URI of the layout mode (optional)") + private URI mode; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + post(getClient(), target, buildModel(target, nextSequenceProperty(), uri, value, title, description, mode)); + print(target); + + return 0; + } + + /** + * Fetches the target document and returns the next free rdf:_N membership property. + * + * @return membership property + */ + protected Property nextSequenceProperty() + { + Model current; + try (Response response = HttpException.check(target, getClient().get(target, ACCEPT_NTRIPLES))) + { + current = response.readEntity(Model.class); + } + + return SequenceNumbers.nextSequenceProperty(current, ResourceFactory.createResource(target.toString())); + } + + /** + * Builds the object block description. + * + * @param target target document URI + * @param seq membership property (rdf:_N) + * @param uri block URI (optional) + * @param value object resource URI + * @param title block title (optional) + * @param description block description (optional) + * @param mode layout mode URI (optional) + * @return block model + */ + public static Model buildModel(URI target, Property seq, String uri, URI value, String title, String description, URI mode) + { + Model model = ModelFactory.createDefaultModel(); + + Resource block = createSubject(model, target, uri). + addProperty(RDF.type, LDH.Object). + addProperty(RDF.value, model.createResource(value.toString())); + model.createResource(target.toString()).addProperty(seq, block); + if (title != null) block.addProperty(DCTerms.title, title); + if (description != null) block.addProperty(DCTerms.description, description); + if (mode != null) block.addProperty(AC.mode, model.createResource(mode.toString())); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java new file mode 100644 index 000000000..7995fd0c2 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java @@ -0,0 +1,106 @@ +/* + * 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.cli.command.content; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.SequenceNumbers; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends an XHTML content block to a document. Mirrors bin/content/add-xhtml-block.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-xhtml-block", description = "Appends an XHTML content block to a document.") +public class AddXHTMLBlock extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; // accepted for script interface parity, unused + + @Option(names = "--value", required = true, paramLabel = "XHTML", description = "XHTML content (must be well-formed XML)") + private String value; + + @Option(names = "--title", paramLabel = "TITLE", description = "Title of the block (optional)") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the block (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the block (optional, blank node if not set)") + private String uri; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + Model current; + try (Response response = HttpException.check(target, getClient().get(target, ACCEPT_NTRIPLES))) + { + current = response.readEntity(Model.class); + } + Property seq = SequenceNumbers.nextSequenceProperty(current, ResourceFactory.createResource(target.toString())); + + post(getClient(), target, buildModel(target, seq, uri, value, title, description)); + print(target); + + return 0; + } + + /** + * Builds the XHTML block description. + * + * @param target target document URI + * @param seq membership property (rdf:_N) + * @param uri block URI (optional) + * @param value XHTML content + * @param title block title (optional) + * @param description block description (optional) + * @return block model + */ + public static Model buildModel(URI target, Property seq, String uri, String value, String title, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource block = createSubject(model, target, uri). + addProperty(RDF.type, LDH.XHTML). + addProperty(RDF.value, model.createTypedLiteral(value, RDF.dtXMLLiteral)); + model.createResource(target.toString()).addProperty(seq, block); + if (title != null) block.addProperty(DCTerms.title, title); + if (description != null) block.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java new file mode 100644 index 000000000..900db7912 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java @@ -0,0 +1,45 @@ +/* + * 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.cli.command.content; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Content block command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "content", + description = "Content block commands.", + subcommands = { AddObjectBlock.class, AddXHTMLBlock.class, RemoveBlock.class }) +public class Content implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java new file mode 100644 index 000000000..dd6bfee7c --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java @@ -0,0 +1,50 @@ +/* + * 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.cli.command.content; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Removes a content block (or all content blocks) from a document. Mirrors bin/content/remove-block.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "remove-block", description = "Removes a content block from a document.") +public class RemoveBlock extends BaseCommand +{ + + @Option(names = "--block", paramLabel = "BLOCK_URI", description = "URI of the content block (optional, all blocks if not set)") + private URI block; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().patch(target, Updates.removeBlock(target, block))).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java new file mode 100644 index 000000000..0decc0d17 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java @@ -0,0 +1,123 @@ +/* + * 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.cli.command.imports; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds CSV import metadata to a document. Mirrors bin/imports/add-csv-import.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-csv-import", description = "Adds CSV import metadata to a document.") +public class AddCSVImport extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the import (optional, blank node if not set)") + private String uri; + + @Option(names = "--query", required = true, paramLabel = "QUERY_URI", description = "URI of the transformation CONSTRUCT query") + private URI query; + + @Option(names = "--file", required = true, paramLabel = "FILE_URI", description = "URI of the uploaded CSV file") + private URI file; + + @Option(names = "--delimiter", defaultValue = ",", paramLabel = "CHAR", description = "CSV delimiter character (default: ${DEFAULT-VALUE})") + private String delimiter; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + core(getClient(), target, uri, title, query, file, delimiter, description); + print(target); + + return 0; + } + + /** + * Appends the CSV import metadata to the target document. + * + * @param client client instance + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param query transformation query URI + * @param file uploaded file URI + * @param delimiter CSV delimiter + * @param description import description (optional) + */ + public static void core(LDHClient client, URI target, String uri, String title, URI query, URI file, String delimiter, String description) + { + post(client, target, buildModel(target, uri, title, query, file, delimiter, description)); + } + + /** + * Builds the CSV import description. + * + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param query transformation query URI + * @param file uploaded file URI + * @param delimiter CSV delimiter + * @param description import description (optional) + * @return import model + */ + public static Model buildModel(URI target, String uri, String title, URI query, URI file, String delimiter, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource csvImport = createSubject(model, target, uri). + addProperty(RDF.type, LDH.CSVImport). + addProperty(DCTerms.title, title). + addProperty(SPIN.query, model.createResource(query.toString())). + addProperty(LDH.file, model.createResource(file.toString())). + addProperty(LDH.delimiter, delimiter); + if (description != null) csvImport.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java new file mode 100644 index 000000000..4b91effcf --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java @@ -0,0 +1,124 @@ +/* + * 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.cli.command.imports; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SD; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds RDF import metadata to a document. Mirrors bin/imports/add-rdf-import.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-rdf-import", description = "Adds RDF import metadata to a document.") +public class AddRDFImport extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the import (optional, blank node if not set)") + private String uri; + + @Option(names = "--query", paramLabel = "QUERY_URI", description = "URI of the transformation CONSTRUCT query (optional)") + private URI query; + + @Option(names = "--graph", paramLabel = "GRAPH_URI", description = "URI of the target named graph (optional)") + private URI graph; + + @Option(names = "--file", required = true, paramLabel = "FILE_URI", description = "URI of the uploaded RDF file") + private URI file; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + core(getClient(), target, uri, title, file, query, graph, description); + print(target); + + return 0; + } + + /** + * Appends the RDF import metadata to the target document. + * + * @param client client instance + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param file uploaded file URI + * @param query transformation query URI (optional) + * @param graph target named graph URI (optional) + * @param description import description (optional) + */ + public static void core(LDHClient client, URI target, String uri, String title, URI file, URI query, URI graph, String description) + { + post(client, target, buildModel(target, uri, title, file, query, graph, description)); + } + + /** + * Builds the RDF import description. + * + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param file uploaded file URI + * @param query transformation query URI (optional) + * @param graph target named graph URI (optional) + * @param description import description (optional) + * @return import model + */ + public static Model buildModel(URI target, String uri, String title, URI file, URI query, URI graph, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource rdfImport = createSubject(model, target, uri). + addProperty(RDF.type, LDH.RDFImport). + addProperty(DCTerms.title, title). + addProperty(LDH.file, model.createResource(file.toString())); + if (graph != null) rdfImport.addProperty(SD.name, model.createResource(graph.toString())); + if (query != null) rdfImport.addProperty(SPIN.query, model.createResource(query.toString())); + if (description != null) rdfImport.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java new file mode 100644 index 000000000..ea4509518 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java @@ -0,0 +1,81 @@ +/* + * 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.cli.command.imports; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.command.AddConstruct; +import com.atomgraph.linkeddatahub.cli.command.AddFile; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Imports CSV data: adds the transformation query, uploads the CSV file and creates the + * import metadata on the target document. Mirrors bin/imports/import-csv.sh, + * calling the same steps in-process instead of via subscripts. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import-csv", description = "Imports CSV data using a transformation query.") +public class ImportCSV extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the transformation CONSTRUCT query") + private Path queryFile; + + @Option(names = "--csv-file", required = true, paramLabel = "ABS_PATH", description = "Path to the CSV file") + private Path csvFile; + + @Option(names = "--delimiter", defaultValue = ",", paramLabel = "CHAR", description = "CSV delimiter character (default: ${DEFAULT-VALUE})") + private String delimiter; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the import document") + private URI target; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + + String queryId = Slugs.defaultSlug(); + AddConstruct.core(getClient(), target, "#" + queryId, title, Files.readString(queryFile), null, null); + URI query = target.resolve("#" + queryId); + + URI fileURI = AddFile.core(getClient(), base, target, csvFile, "text/csv", title, null); + + AddCSVImport.core(getClient(), target, null, title, query, fileURI, delimiter, description); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java new file mode 100644 index 000000000..db3cbed84 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java @@ -0,0 +1,89 @@ +/* + * 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.cli.command.imports; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.command.AddConstruct; +import com.atomgraph.linkeddatahub.cli.command.AddFile; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Imports RDF data: optionally adds a transformation query, uploads the RDF file and creates + * the import metadata on the target document. Mirrors bin/imports/import-rdf.sh, + * calling the same steps in-process instead of via subscripts. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import-rdf", description = "Imports RDF data, optionally using a transformation query.") +public class ImportRDF extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--query-file", paramLabel = "ABS_PATH", description = "Path to the file with the transformation CONSTRUCT query (optional)") + private Path queryFile; + + @Option(names = "--rdf-file", required = true, paramLabel = "ABS_PATH", description = "Path to the RDF file") + private Path rdfFile; + + @Option(names = "--content-type", required = true, paramLabel = "MEDIA_TYPE", description = "Media type of the RDF file (e.g. text/turtle)") + private String contentType; + + @Option(names = "--graph", paramLabel = "GRAPH_URI", description = "URI of the target named graph (optional)") + private URI graph; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the import document") + private URI target; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + + URI query = null; + if (queryFile != null) + { + String queryId = Slugs.defaultSlug(); + AddConstruct.core(getClient(), target, "#" + queryId, title, Files.readString(queryFile), null, null); + query = target.resolve("#" + queryId); + } + + URI fileURI = AddFile.core(getClient(), base, target, rdfFile, contentType, title, null); + + String importId = Slugs.defaultSlug(); + AddRDFImport.core(getClient(), target, "#" + importId, title, fileURI, query, query == null ? graph : null, description); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java new file mode 100644 index 000000000..65b084d19 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java @@ -0,0 +1,45 @@ +/* + * 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.cli.command.imports; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Data import command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "imports", + description = "Data import commands.", + subcommands = { AddCSVImport.class, AddRDFImport.class, ImportCSV.class, ImportRDF.class }) +public class Imports implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java new file mode 100644 index 000000000..2a7cf1f24 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java @@ -0,0 +1,127 @@ +/* + * 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.cli.http; + +import com.atomgraph.core.io.ModelProvider; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +/** + * Builds a Jersey HTTP client authenticated with a WebID client certificate from a PKCS12 keystore. + * Mirrors Application.getClient() in LinkedDataHub, with server certificate checks + * disabled (equivalent of curl -k against self-signed dev instances). + * + * @author Martynas Jusevičius {@literal } + */ +public final class ClientFactory +{ + + private ClientFactory() { } + + /** + * Builds the client instance. + * + * @param keyStoreFile PKCS12 (.p12) keystore file with the WebID certificate + * @param keyStorePassword keystore password + * @return client instance + */ + public static Client createClient(Path keyStoreFile, String keyStorePassword) + { + SSLContext ctx; + try + { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = Files.newInputStream(keyStoreFile)) + { + keyStore.load(is, keyStorePassword.toCharArray()); + } + + // for client authentication + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keyStorePassword.toCharArray()); + + ctx = SSLContext.getInstance("TLS"); + ctx.init(kmf.getKeyManagers(), new TrustManager[] { TRUST_ALL }, new SecureRandom()); + } + catch (IOException | GeneralSecurityException ex) + { + throw new IllegalArgumentException("Could not load PKCS12 keystore '" + keyStoreFile + "': " + ex.getMessage() + " (wrong password?)", ex); + } + + Registry socketFactoryRegistry = RegistryBuilder.create(). + register("https", new SSLConnectionSocketFactory(ctx, NoopHostnameVerifier.INSTANCE)). + register("http", new PlainConnectionSocketFactory()). + build(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(new ApacheConnectorProvider()); + config.register(MultiPartFeature.class); + config.register(new ModelProvider()); + config.property(ClientProperties.FOLLOW_REDIRECTS, false); // scripts use curl without -L + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); + config.property(ApacheClientProperties.CONNECTION_MANAGER, new PoolingHttpClientConnectionManager(socketFactoryRegistry)); + + return ClientBuilder.newBuilder(). + withConfig(config). + sslContext(ctx). + hostnameVerifier(NoopHostnameVerifier.INSTANCE). + build(); + } + + private static final X509TrustManager TRUST_ALL = new X509TrustManager() + { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[0]; + } + + }; + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java new file mode 100644 index 000000000..a4bf71701 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java @@ -0,0 +1,102 @@ +/* + * 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.cli.http; + +import jakarta.ws.rs.core.Response; +import java.net.URI; + +/** + * Thrown when the server responds with an error status, mirroring curl -f. + * + * @author Martynas Jusevičius {@literal } + */ +public class HttpException extends RuntimeException +{ + + private static final int BODY_EXCERPT_LENGTH = 1024; + + private final int status; + private final URI uri; + + /** + * Constructs the exception. + * + * @param status HTTP status code + * @param reasonPhrase HTTP reason phrase + * @param uri request URI + * @param body response body excerpt (can be empty) + */ + public HttpException(int status, String reasonPhrase, URI uri, String body) + { + super("HTTP " + status + " " + reasonPhrase + " — " + uri + (body == null || body.isBlank() ? "" : "\n" + body)); + this.status = status; + this.uri = uri; + } + + /** + * Throws if the response has an error status (≥ 400), otherwise returns it. + * + * @param uri request URI (for the error message) + * @param response response to check + * @return the same response + */ + public static Response check(URI uri, Response response) + { + if (response.getStatus() >= 400) + { + String body = ""; + try + { + body = response.readEntity(String.class); + if (body.length() > BODY_EXCERPT_LENGTH) body = body.substring(0, BODY_EXCERPT_LENGTH) + "…"; + } + catch (Exception ex) + { + // body is unreadable - leave the excerpt empty + } + finally + { + response.close(); + } + + throw new HttpException(response.getStatus(), response.getStatusInfo().getReasonPhrase(), uri, body); + } + + return response; + } + + /** + * Returns the HTTP status code. + * + * @return status code + */ + public int getStatus() + { + return status; + } + + /** + * Returns the request URI. + * + * @return request URI + */ + public URI getUri() + { + return uri; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java new file mode 100644 index 000000000..c62a0a4dd --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java @@ -0,0 +1,100 @@ +/* + * 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.cli.http; + +import com.atomgraph.core.MediaTypes; +import com.atomgraph.core.client.GraphStoreClient; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; + +/** + * Graph Store Protocol client for LinkedDataHub documents (direct graph identification), + * extended with SPARQL update over PATCH and form-encoded POST. When a proxy URI is given, + * every request URI has its origin rewritten to the proxy's origin, matching the + * --proxy handling of the bin/ shell scripts. + * + * @author Martynas Jusevičius {@literal } + */ +public class LDHClient extends GraphStoreClient +{ + + /** SPARQL update media type */ + public static final MediaType APPLICATION_SPARQL_UPDATE_TYPE = MediaType.valueOf("application/sparql-update"); + + private final URI proxy; + + /** + * Constructs the client. + * + * @param client Jersey client with WebID client certificate + * @param mediaTypes registry of readable/writable media types + * @param proxy proxy URI whose origin replaces the request URI origin (optional, can be null) + */ + public LDHClient(Client client, MediaTypes mediaTypes, URI proxy) + { + super(client, mediaTypes); + this.proxy = proxy; + } + + @Override + protected WebTarget getWebTarget(URI uri) + { + return super.getWebTarget(getProxy() != null ? URIRewriter.rewrite(uri, getProxy()) : uri); + } + + /** + * Patches a document with a SPARQL update. + * + * @param uri document URI + * @param update SPARQL update string + * @return response + */ + public Response patch(URI uri, String update) + { + return getWebTarget(uri).request().method("PATCH", Entity.entity(update, APPLICATION_SPARQL_UPDATE_TYPE)); + } + + /** + * Posts a form-encoded request. + * + * @param uri target URI + * @param form form params + * @param acceptedTypes accepted response media types + * @return response + */ + public Response postForm(URI uri, Form form, MediaType... acceptedTypes) + { + return getWebTarget(uri).request(acceptedTypes).post(Entity.form(form)); + } + + /** + * Returns the proxy URI, if any. + * + * @return proxy URI or null + */ + public URI getProxy() + { + return proxy; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java new file mode 100644 index 000000000..5b56680cf --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java @@ -0,0 +1,59 @@ +/* + * 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.cli.mixin; + +import java.net.URI; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * Application base URI option shared by commands whose script counterpart takes -b. + * + * @author Martynas Jusevičius {@literal } + */ +public class BaseMixin +{ + + @Option(names = {"-b", "--base"}, defaultValue = "${env:LDH_BASE}", paramLabel = "BASE_URI", + description = "Base URI of the application (env: LDH_BASE)") + private URI base; + + /** + * Validates presence of the base URI and returns it. + * + * @param spec command spec used to raise usage errors + * @return base URI + */ + public URI require(CommandSpec spec) + { + if (base == null) throw new ParameterException(spec.commandLine(), "Missing required option: '--base=BASE_URI' (or set LDH_BASE)"); + + return base; + } + + /** + * Returns the base URI, if any. + * + * @return base URI or null + */ + public URI getBase() + { + return base; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java new file mode 100644 index 000000000..23ac29efa --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java @@ -0,0 +1,71 @@ +/* + * 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.cli.mixin; + +import java.nio.file.Path; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * WebID client certificate options shared by all commands. + * + * @author Martynas Jusevičius {@literal } + */ +public class CertAuthMixin +{ + + @Option(names = {"-f", "--cert-file"}, defaultValue = "${env:LDH_CERT_FILE}", paramLabel = "CERT_FILE", + description = ".p12 (PKCS12) keystore with the WebID certificate of the agent (env: LDH_CERT_FILE)") + private Path certFile; + + @Option(names = {"-p", "--cert-password"}, defaultValue = "${env:LDH_CERT_PASSWORD}", paramLabel = "CERT_PASSWORD", + description = "Password of the WebID certificate (env: LDH_CERT_PASSWORD)") + private String certPassword; + + /** + * Validates that both certificate options are present. + * + * @param spec command spec used to raise usage errors + */ + public void validate(CommandSpec spec) + { + if (certFile == null) throw new ParameterException(spec.commandLine(), "Missing required option: '--cert-file=CERT_FILE' (or set LDH_CERT_FILE)"); + if (certPassword == null) throw new ParameterException(spec.commandLine(), "Missing required option: '--cert-password=CERT_PASSWORD' (or set LDH_CERT_PASSWORD)"); + } + + /** + * Returns the keystore path. + * + * @return keystore path + */ + public Path getCertFile() + { + return certFile; + } + + /** + * Returns the keystore password. + * + * @return keystore password + */ + public String getCertPassword() + { + return certPassword; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java new file mode 100644 index 000000000..876a8d54a --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java @@ -0,0 +1,44 @@ +/* + * 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.cli.mixin; + +import java.net.URI; +import picocli.CommandLine.Option; + +/** + * Proxy option shared by all commands. + * + * @author Martynas Jusevičius {@literal } + */ +public class ProxyMixin +{ + + @Option(names = "--proxy", defaultValue = "${env:LDH_PROXY}", paramLabel = "PROXY_URL", + description = "The host this request will be proxied through (optional) (env: LDH_PROXY)") + private URI proxy; + + /** + * Returns the proxy URI, if any. + * + * @return proxy URI or null + */ + public URI getProxy() + { + return proxy; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java new file mode 100644 index 000000000..cb621e377 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java @@ -0,0 +1,151 @@ +/* + * 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.cli.sparql; + +import java.net.URI; +import org.apache.jena.query.ParameterizedSparqlString; + +/** + * SPARQL update templates for the PATCH-based commands. All templates are standard SPARQL 1.1; + * IRIs are injected via {@link ParameterizedSparqlString} to guarantee correct escaping. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Updates +{ + + private Updates() { } + + /** + * Adds an owl:imports statement to the ontology that is the primary topic + * of the given document. + * + * @param ontologyDoc ontology document URI + * @param importURI imported ontology URI + * @return SPARQL update string + */ + public static String insertOntologyImport(URI ontologyDoc, URI importURI) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX owl: + PREFIX foaf: + INSERT { + ?ontology owl:imports ?import . + } + WHERE { + ?doc foaf:primaryTopic ?ontology . + } + """); + pss.setIri("doc", ontologyDoc.toString()); + pss.setIri("import", importURI.toString()); + return pss.toString(); + } + + /** + * Adds a foaf:member statement to the group that is the primary topic + * of the given document. + * + * @param groupDoc group document URI + * @param agent agent URI + * @return SPARQL update string + */ + public static String insertGroupMember(URI groupDoc, URI agent) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX foaf: + INSERT { + ?group foaf:member ?agent . + } + WHERE { + ?doc foaf:primaryTopic ?group . + } + """); + pss.setIri("doc", groupDoc.toString()); + pss.setIri("agent", agent.toString()); + return pss.toString(); + } + + /** + * Removes a content block (or all content blocks) from a document, together with the + * block's own description. + * + * @param doc document URI + * @param block block URI, or null to remove all blocks + * @return SPARQL update string + */ + public static String removeBlock(URI doc, URI block) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX rdf: + + DELETE + { + ?doc ?seq ?block . + ?block ?p ?o . + } + WHERE + { + ?doc ?seq ?block . + FILTER(strstarts(str(?seq), concat(str(rdf:), "_"))) + OPTIONAL + { + ?block ?p ?o + } + } + """); + pss.setIri("doc", doc.toString()); + if (block != null) pss.setIri("block", block.toString()); + return pss.toString(); + } + + /** + * Makes all end-user application documents publicly readable and allows queries over POST, + * by extending the built-in public authorization. + * + * @param base end-user application base URI + * @param adminBase admin application base URI + * @return SPARQL update string + */ + public static String makePublic(URI base, URI adminBase) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX acl: + PREFIX def: + PREFIX dh: + PREFIX nfo: + PREFIX foaf: + + INSERT + { + ?public acl:accessToClass def:Root, dh:Container, dh:Item, nfo:FileDataObject ; + acl:accessTo ?sparql . + + ?sparqlPost a acl:Authorization ; + acl:accessTo ?sparql ; + acl:mode acl:Append ; + acl:agentClass foaf:Agent, acl:AuthenticatedAgent . # hacky way to allow queries over POST + } + WHERE + {} + """); + pss.setIri("public", adminBase + "acl/authorizations/public/#this"); + pss.setIri("sparqlPost", adminBase + "acl/authorizations/public/#sparql-post"); + pss.setIri("sparql", base + "sparql"); + return pss.toString(); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java new file mode 100644 index 000000000..dd1638251 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java @@ -0,0 +1,69 @@ +/* + * 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.cli.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +/** + * File digest helpers. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Digests +{ + + private Digests() { } + + /** + * Returns the lowercase hex SHA1 digest of a file, matching shasum -a 1. + * + * @param file file path + * @return hex digest string + */ + public static String sha1Hex(Path file) + { + try + { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + try (InputStream is = Files.newInputStream(file)) + { + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) != -1) md.update(buffer, 0, read); + } + + return HexFormat.of().formatHex(md.digest()); + } + catch (NoSuchAlgorithmException ex) + { + throw new IllegalStateException(ex); + } + catch (IOException ex) + { + throw new UncheckedIOException("Could not read file '" + file + "'", ex); + } + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java new file mode 100644 index 000000000..22fcc3738 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java @@ -0,0 +1,75 @@ +/* + * 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.cli.util; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * RDF container membership property (rdf:_N) helpers for content blocks. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SequenceNumbers +{ + + private SequenceNumbers() { } + + /** + * Returns the next free membership property rdf:_(max + 1) for the given subject, + * where max is the highest existing rdf:_N predicate (0 if none). + * + * @param model model with the subject's description + * @param subject resource whose membership properties are scanned + * @return next membership property + */ + public static Property nextSequenceProperty(Model model, Resource subject) + { + String prefix = RDF.getURI() + "_"; + int max = 0; + + StmtIterator it = model.listStatements(subject, null, (RDFNode)null); + try + { + while (it.hasNext()) + { + String uri = it.next().getPredicate().getURI(); + if (uri.startsWith(prefix)) + try + { + max = Math.max(max, Integer.parseInt(uri.substring(prefix.length()))); + } + catch (NumberFormatException ex) + { + // not a membership property, e.g. rdf:_x + } + } + } + finally + { + it.close(); + } + + return ResourceFactory.createProperty(prefix + (max + 1)); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java new file mode 100644 index 000000000..6d77b04ed --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java @@ -0,0 +1,42 @@ +/* + * 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.cli.util; + +import java.util.Locale; +import java.util.UUID; + +/** + * Default URI path segment generation. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Slugs +{ + + private Slugs() { } + + /** + * Returns a random lowercase UUID slug, matching uuidgen | tr '[:upper:]' '[:lower:]'. + * + * @return slug string + */ + public static String defaultSlug() + { + return UUID.randomUUID().toString().toLowerCase(Locale.ROOT); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java new file mode 100644 index 000000000..593181c03 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java @@ -0,0 +1,104 @@ +/* + * 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.cli.util; + +import java.net.URI; +import java.nio.charset.StandardCharsets; + +/** + * URI manipulation helpers matching the conventions of the bin/ shell scripts. + * + * @author Martynas Jusevičius {@literal } + */ +public final class URIRewriter +{ + + private URIRewriter() { } + + /** + * Replaces the origin (scheme and authority) of a URI with the origin of the proxy URI, + * keeping the path, query and fragment. + * + * @param uri logical URI + * @param proxy proxy URI whose origin is substituted + * @return rewritten URI + */ + public static URI rewrite(URI uri, URI proxy) + { + return URI.create(origin(proxy) + uri.toString().substring(origin(uri).length())); + } + + /** + * Returns the origin (scheme and authority) of a URI. + * + * @param uri URI + * @return origin string, e.g. https://localhost:4443 + */ + public static String origin(URI uri) + { + if (uri.getScheme() == null || uri.getRawAuthority() == null) throw new IllegalArgumentException("URI '" + uri + "' is not absolute"); + + return uri.getScheme() + "://" + uri.getRawAuthority(); + } + + /** + * Converts an end-user application base URI to the base URI of its admin application + * by prefixing the host with the admin. subdomain. + * + * @param base end-user base URI + * @return admin base URI + */ + public static URI adminBase(URI base) + { + return URI.create(base.toString().replaceFirst("://", "://admin.")); + } + + /** + * Percent-encodes a string as a URI path segment. All characters except RFC 3986 + * unreserved ones are encoded, including /. + * + * @param slug path segment + * @return encoded path segment + */ + public static String encodeSlug(String slug) + { + StringBuilder sb = new StringBuilder(); + + for (byte b : slug.getBytes(StandardCharsets.UTF_8)) + { + char c = (char)(b & 0xFF); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + c == '-' || c == '.' || c == '_' || c == '~') sb.append(c); + else sb.append('%').append(String.format("%02X", b & 0xFF)); + } + + return sb.toString(); + } + + /** + * Builds the URI of a child document from the parent container URI and a path segment slug. + * + * @param parent parent container URI (with trailing slash) + * @param slug path segment + * @return child document URI (with trailing slash) + */ + public static URI childURI(URI parent, String slug) + { + return URI.create(parent.toString() + encodeSlug(slug) + "/"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java new file mode 100644 index 000000000..73dd5abd3 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java @@ -0,0 +1,42 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * AtomGraph Core vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class A +{ + + /** Namespace URI */ + public static final String NS = "https://w3id.org/atomgraph/core#"; + + private A() { } + + /** a:graphStore property */ + public static final Property graphStore = ResourceFactory.createProperty(NS + "graphStore"); + /** a:authUser property */ + public static final Property authUser = ResourceFactory.createProperty(NS + "authUser"); + /** a:authPwd property */ + public static final Property authPwd = ResourceFactory.createProperty(NS + "authPwd"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java new file mode 100644 index 000000000..9de56512e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java @@ -0,0 +1,38 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * AtomGraph Client vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class AC +{ + + /** Namespace URI */ + public static final String NS = "https://w3id.org/atomgraph/client#"; + + private AC() { } + + /** ac:mode property */ + public static final Property mode = ResourceFactory.createProperty(NS + "mode"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java new file mode 100644 index 000000000..e7f3ff709 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java @@ -0,0 +1,60 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * W3C Web Access Control vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class ACL +{ + + /** Namespace URI */ + public static final String NS = "http://www.w3.org/ns/auth/acl#"; + + private ACL() { } + + /** acl:Authorization class */ + public static final Resource Authorization = ResourceFactory.createResource(NS + "Authorization"); + /** acl:Append mode */ + public static final Resource Append = ResourceFactory.createResource(NS + "Append"); + /** acl:Control mode */ + public static final Resource Control = ResourceFactory.createResource(NS + "Control"); + /** acl:Read mode */ + public static final Resource Read = ResourceFactory.createResource(NS + "Read"); + /** acl:Write mode */ + public static final Resource Write = ResourceFactory.createResource(NS + "Write"); + + /** acl:agent property */ + public static final Property agent = ResourceFactory.createProperty(NS + "agent"); + /** acl:agentClass property */ + public static final Property agentClass = ResourceFactory.createProperty(NS + "agentClass"); + /** acl:agentGroup property */ + public static final Property agentGroup = ResourceFactory.createProperty(NS + "agentGroup"); + /** acl:accessTo property */ + public static final Property accessTo = ResourceFactory.createProperty(NS + "accessTo"); + /** acl:accessToClass property */ + public static final Property accessToClass = ResourceFactory.createProperty(NS + "accessToClass"); + /** acl:mode property */ + public static final Property mode = ResourceFactory.createProperty(NS + "mode"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java new file mode 100644 index 000000000..5829aae8e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java @@ -0,0 +1,40 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * Document hierarchy vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class DH +{ + + /** Namespace URI */ + public static final String NS = "https://www.w3.org/ns/ldt/document-hierarchy#"; + + private DH() { } + + /** dh:Item class */ + public static final Resource Item = ResourceFactory.createResource(NS + "Item"); + /** dh:Container class */ + public static final Resource Container = ResourceFactory.createResource(NS + "Container"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java new file mode 100644 index 000000000..d25632751 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java @@ -0,0 +1,68 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * LinkedDataHub vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class LDH +{ + + /** Namespace URI */ + public static final String NS = "https://w3id.org/atomgraph/linkeddatahub#"; + + private LDH() { } + + /** ldh:Object class */ + public static final Resource Object = ResourceFactory.createResource(NS + "Object"); + /** ldh:XHTML class */ + public static final Resource XHTML = ResourceFactory.createResource(NS + "XHTML"); + /** ldh:View class */ + public static final Resource View = ResourceFactory.createResource(NS + "View"); + /** ldh:ResultSetChart class */ + public static final Resource ResultSetChart = ResourceFactory.createResource(NS + "ResultSetChart"); + /** ldh:CSVImport class */ + public static final Resource CSVImport = ResourceFactory.createResource(NS + "CSVImport"); + /** ldh:RDFImport class */ + public static final Resource RDFImport = ResourceFactory.createResource(NS + "RDFImport"); + /** ldh:MissingPropertyValue constraint class */ + public static final Resource MissingPropertyValue = ResourceFactory.createResource(NS + "MissingPropertyValue"); + /** ldh:ChildrenView resource */ + public static final Resource ChildrenView = ResourceFactory.createResource(NS + "ChildrenView"); + /** ldh:SelectChildren query resource */ + public static final Resource SelectChildren = ResourceFactory.createResource(NS + "SelectChildren"); + + /** ldh:service property */ + public static final Property service = ResourceFactory.createProperty(NS + "service"); + /** ldh:file property */ + public static final Property file = ResourceFactory.createProperty(NS + "file"); + /** ldh:delimiter property */ + public static final Property delimiter = ResourceFactory.createProperty(NS + "delimiter"); + /** ldh:chartType property */ + public static final Property chartType = ResourceFactory.createProperty(NS + "chartType"); + /** ldh:categoryVarName property */ + public static final Property categoryVarName = ResourceFactory.createProperty(NS + "categoryVarName"); + /** ldh:seriesVarName property */ + public static final Property seriesVarName = ResourceFactory.createProperty(NS + "seriesVarName"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java new file mode 100644 index 000000000..de6a53c65 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java @@ -0,0 +1,42 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * NEPOMUK File Ontology vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class NFO +{ + + /** Namespace URI */ + public static final String NS = "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#"; + + private NFO() { } + + /** nfo:FileDataObject class */ + public static final Resource FileDataObject = ResourceFactory.createResource(NS + "FileDataObject"); + + /** nfo:fileName property */ + public static final Property fileName = ResourceFactory.createProperty(NS + "fileName"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java new file mode 100644 index 000000000..4bec9d26f --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java @@ -0,0 +1,50 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * SPARQL 1.1 Service Description vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SD +{ + + /** Namespace URI */ + public static final String NS = "http://www.w3.org/ns/sparql-service-description#"; + + private SD() { } + + /** sd:Service class */ + public static final Resource Service = ResourceFactory.createResource(NS + "Service"); + /** sd:SPARQL11Query language */ + public static final Resource SPARQL11Query = ResourceFactory.createResource(NS + "SPARQL11Query"); + /** sd:SPARQL11Update language */ + public static final Resource SPARQL11Update = ResourceFactory.createResource(NS + "SPARQL11Update"); + + /** sd:endpoint property */ + public static final Property endpoint = ResourceFactory.createProperty(NS + "endpoint"); + /** sd:supportedLanguage property */ + public static final Property supportedLanguage = ResourceFactory.createProperty(NS + "supportedLanguage"); + /** sd:name property */ + public static final Property name = ResourceFactory.createProperty(NS + "name"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java new file mode 100644 index 000000000..6634441f9 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java @@ -0,0 +1,46 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * SPIN SPARQL syntax vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SP +{ + + /** Namespace URI */ + public static final String NS = "http://spinrdf.org/sp#"; + + private SP() { } + + /** sp:Construct class */ + public static final Resource Construct = ResourceFactory.createResource(NS + "Construct"); + /** sp:Select class */ + public static final Resource Select = ResourceFactory.createResource(NS + "Select"); + + /** sp:text property */ + public static final Property text = ResourceFactory.createProperty(NS + "text"); + /** sp:arg1 property */ + public static final Property arg1 = ResourceFactory.createProperty(NS + "arg1"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java new file mode 100644 index 000000000..a4cbec33d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java @@ -0,0 +1,42 @@ +/* + * 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.cli.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * SPIN modeling vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SPIN +{ + + /** Namespace URI */ + public static final String NS = "http://spinrdf.org/spin#"; + + private SPIN() { } + + /** spin:query property */ + public static final Property query = ResourceFactory.createProperty(NS + "query"); + /** spin:constructor property */ + public static final Property constructor = ResourceFactory.createProperty(NS + "constructor"); + /** spin:constraint property */ + public static final Property constraint = ResourceFactory.createProperty(NS + "constraint"); + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java new file mode 100644 index 000000000..f42ccf76d --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java @@ -0,0 +1,116 @@ +/* + * 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.cli; + +import java.io.PrintWriter; +import java.io.Writer; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; +import picocli.CommandLine.ParseResult; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for the command tree and argument parsing. + */ +public class CommandParsingTest +{ + + static CommandLine commandLine() + { + CommandLine cmd = new CommandLine(new LDH()); + cmd.setOut(new PrintWriter(Writer.nullWriter())); + cmd.setErr(new PrintWriter(Writer.nullWriter())); + return cmd; + } + + @Test + public void commandTreeMirrorsScriptLayout() + { + CommandLine root = commandLine(); + + List.of("get", "post", "put", "patch", "delete", "create-item", "create-container", + "add-view", "add-construct", "add-select", "add-result-set-chart", "add-file", "add-generic-service", + "admin", "content", "imports"). + forEach(name -> assertTrue(root.getSubcommands().containsKey(name), name)); + + CommandLine admin = root.getSubcommands().get("admin"); + List.of("ontologies", "acl", "packages", "clear-ontology", "add-ontology-import"). + forEach(name -> assertTrue(admin.getSubcommands().containsKey(name), name)); + + List.of("create-ontology", "import-ontology", "add-class", "add-constructor", "add-select", + "add-property-constraint", "add-restriction"). + forEach(name -> assertTrue(admin.getSubcommands().get("ontologies").getSubcommands().containsKey(name), name)); + + List.of("create-group", "create-authorization", "add-agent-to-group", "make-public"). + forEach(name -> assertTrue(admin.getSubcommands().get("acl").getSubcommands().containsKey(name), name)); + + List.of("install-package", "uninstall-package"). + forEach(name -> assertTrue(admin.getSubcommands().get("packages").getSubcommands().containsKey(name), name)); + + List.of("add-object-block", "add-xhtml-block", "remove-block"). + forEach(name -> assertTrue(root.getSubcommands().get("content").getSubcommands().containsKey(name), name)); + + List.of("add-csv-import", "add-rdf-import", "import-csv", "import-rdf"). + forEach(name -> assertTrue(root.getSubcommands().get("imports").getSubcommands().containsKey(name), name)); + } + + @Test + public void missingRequiredOptionIsUsageError() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("create-item", "--container", "https://localhost:4443/some/")); + } + + @Test + public void unknownOptionIsUsageError() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("get", "--bogus")); + } + + @Test + public void bareGroupCommandIsUsageError() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin", "acl")); + } + + @Test + public void repeatableOptionsAccumulate() + { + ParseResult parseResult = commandLine().parseArgs("admin", "acl", "create-group", + "-f", "cert.p12", "-p", "secret", "-b", "https://admin.localhost:4443/", + "--name", "Editors", + "--member", "https://localhost:4443/acl/agents/a/#this", + "--member", "https://localhost:4443/acl/agents/b/#this"); + + ParseResult createGroup = parseResult.subcommand().subcommand().subcommand(); + List members = createGroup.matchedOption("--member").getValue(); + assertEquals(2, members.size()); + } + + @Test + public void missingCertOptionsFailValidationAtExecutionTime() + { + // cert options have env-var defaults, so they are validated at execution time, not parse time + assertNotNull(commandLine().parseArgs("delete", "https://localhost:4443/some/")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("delete", "https://localhost:4443/some/")); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java new file mode 100644 index 000000000..a40c4ae8f --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java @@ -0,0 +1,71 @@ +/* + * 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.cli.command; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the RDF/POST multipart encoding of {@link AddFile}: the field order is positional, + * each pu must immediately precede its ol/ou value. + */ +public class AddFileMultiPartTest +{ + + @TempDir + Path tempDir; + + @Test + public void fieldOrderIsPositional() throws Exception + { + Path file = tempDir.resolve("data.csv"); + Files.writeString(file, "a,b\n1,2\n"); + + try (FormDataMultiPart multiPart = AddFile.buildMultiPart(file, "text/csv", "Data", null)) + { + List names = multiPart.getBodyParts().stream(). + map(part -> ((FormDataBodyPart)part).getName()). + toList(); + + assertEquals(List.of("rdf", "sb", "pu", "ol", "pu", "ol", "pu", "ou"), names); + assertEquals("text/csv", multiPart.getBodyParts().get(3).getMediaType().toString()); + } + } + + @Test + public void descriptionAppendsTrailingPair() throws Exception + { + Path file = tempDir.resolve("data.csv"); + Files.writeString(file, "a,b\n1,2\n"); + + try (FormDataMultiPart multiPart = AddFile.buildMultiPart(file, "text/csv", "Data", "Description")) + { + List names = multiPart.getBodyParts().stream(). + map(part -> ((FormDataBodyPart)part).getName()). + toList(); + + assertEquals(List.of("rdf", "sb", "pu", "ol", "pu", "ol", "pu", "ou", "pu", "ol"), names); + } + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java new file mode 100644 index 000000000..30a4b84cc --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java @@ -0,0 +1,240 @@ +/* + * 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.cli.command; + +import com.atomgraph.linkeddatahub.cli.command.admin.acl.CreateAuthorization; +import com.atomgraph.linkeddatahub.cli.command.admin.acl.CreateGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.CreateOntology; +import com.atomgraph.linkeddatahub.cli.command.content.AddXHTMLBlock; +import com.atomgraph.linkeddatahub.cli.command.imports.AddCSVImport; +import com.atomgraph.linkeddatahub.cli.command.imports.AddRDFImport; +import com.atomgraph.linkeddatahub.cli.vocab.ACL; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.io.StringReader; +import java.net.URI; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Compares command model builders against the Turtle bodies of the original shell scripts. + */ +public class ModelBuildersTest +{ + + private static final URI TARGET = URI.create("https://localhost:4443/some/"); + private static final String PREFIXES = """ + @prefix dh: . + @prefix ldh: . + @prefix rdf: . + @prefix dct: . + @prefix spin: . + @prefix sp: . + @prefix ac: . + @prefix acl: . + @prefix sd: . + @prefix owl: . + @prefix rdfs: . + @prefix foaf: . + """; + + static Model parse(String turtle) + { + Model model = ModelFactory.createDefaultModel(); + RDFParser.create().source(new StringReader(PREFIXES + turtle)).lang(Lang.TURTLE).base(TARGET.toString()).parse(model); + return model; + } + + static void assertIsomorphic(Model expected, Model actual) + { + assertTrue(actual.isIsomorphicWith(expected), + "Expected:\n" + expected.toString() + "\nActual:\n" + actual.toString()); + } + + @Test + public void createItem() + { + URI doc = URI.create("https://localhost:4443/some/my-item/"); + + assertIsomorphic(parse(""" + a dh:Item ; + dct:title "My item" ; + dct:description "Desc" . + """), + CreateItem.buildModel(doc, "My item", "Desc")); + } + + @Test + public void createContainerDefaultChildrenView() + { + assertIsomorphic(parse(""" + <> a dh:Container ; + dct:title "Some" ; + rdf:_1 [ a ldh:Object ; rdf:value ldh:ChildrenView ] . + """), + CreateContainer.buildModel(TARGET, "Some", null, null, null)); + } + + @Test + public void createContainerWithMode() + { + assertIsomorphic(parse(""" + <> a dh:Container ; + dct:title "Some" ; + rdf:_1 [ a ldh:Object ; rdf:value [ a ldh:View ; spin:query ldh:SelectChildren ; ac:mode ] ] . + """), + CreateContainer.buildModel(TARGET, "Some", null, null, URI.create("https://w3id.org/atomgraph/client#GridMode"))); + } + + @Test + public void createContainerWithBlock() + { + assertIsomorphic(parse(""" + <> a dh:Container ; + dct:title "Some" ; + rdf:_1 . + """), + CreateContainer.buildModel(TARGET, "Some", null, URI.create("https://localhost:4443/some/#block"), null)); + } + + @Test + public void addViewWithURIAndMode() + { + assertIsomorphic(parse(""" + <#view> a ldh:View ; + spin:query ; + dct:title "View" ; + ac:mode . + """), + AddView.buildModel(TARGET, "#view", URI.create("https://localhost:4443/queries/q/#this"), "View", null, + URI.create("https://w3id.org/atomgraph/client#GridMode"))); + } + + @Test + public void addConstructQuery() + { + assertIsomorphic(parse(""" + _:q a sp:Construct ; + dct:title "Query" ; + sp:text \"""CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }\""" ; + ldh:service . + """), + AddConstruct.buildModel(TARGET, null, SP.Construct, "Query", "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }", + URI.create("https://localhost:4443/services/s/#this"), null)); + } + + @Test + public void createOntology() + { + URI doc = URI.create("https://admin.localhost:4443/ontologies/my-ont/"); + + assertIsomorphic(parse(""" + @base . + _:ontology a owl:Ontology ; + rdfs:label "My ontology" ; + rdfs:comment "Comment" . + <> a dh:Item ; + foaf:primaryTopic _:ontology ; + dct:title "My ontology" . + """), + CreateOntology.buildModel(doc, null, "My ontology", "Comment")); + } + + @Test + public void createGroup() + { + URI doc = URI.create("https://admin.localhost:4443/acl/groups/editors/"); + + assertIsomorphic(parse(""" + @base . + _:group a foaf:Group ; + foaf:name "Editors" ; + foaf:member , . + <> a dh:Item ; + foaf:primaryTopic _:group ; + dct:title "Editors" . + """), + CreateGroup.buildModel(doc, null, "Editors", null, + List.of(URI.create("https://localhost:4443/acl/agents/a/#this"), URI.create("https://localhost:4443/acl/agents/b/#this")))); + } + + @Test + public void createAuthorization() + { + URI doc = URI.create("https://admin.localhost:4443/acl/authorizations/auth/"); + + assertIsomorphic(parse(""" + @base . + _:auth a acl:Authorization ; + rdfs:label "Auth" ; + acl:agent ; + acl:accessTo ; + acl:mode acl:Read, acl:Write . + <> a dh:Item ; + foaf:primaryTopic _:auth ; + dct:title "Auth" . + """), + CreateAuthorization.buildModel(doc, null, "Auth", null, + List.of(URI.create("https://localhost:4443/acl/agents/a/#this")), List.of(), List.of(), + List.of(URI.create("https://localhost:4443/some/")), List.of(), + List.of(ACL.Read, ACL.Write))); + } + + @Test + public void addCSVImport() + { + assertIsomorphic(parse(""" + _:import a ldh:CSVImport ; + dct:title "Cities" ; + spin:query ; + ldh:file ; + ldh:delimiter "," . + """), + AddCSVImport.buildModel(TARGET, null, "Cities", URI.create("https://localhost:4443/some/#query"), + URI.create("https://localhost:4443/uploads/abc"), ",", null)); + } + + @Test + public void addRDFImportWithGraph() + { + assertIsomorphic(parse(""" + <#import> a ldh:RDFImport ; + dct:title "Data" ; + ldh:file ; + sd:name . + """), + AddRDFImport.buildModel(TARGET, "#import", "Data", URI.create("https://localhost:4443/uploads/abc"), + null, URI.create("https://localhost:4443/graphs/g/"), null)); + } + + @Test + public void addXHTMLBlock() + { + assertIsomorphic(parse(""" + <> rdf:_4 _:block . + _:block a ldh:XHTML ; + rdf:value "

Hello

"^^rdf:XMLLiteral . + """), + AddXHTMLBlock.buildModel(TARGET, RDF.li(4), null, "

Hello

", null, null)); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.java new file mode 100644 index 000000000..9fd10ba3d --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.java @@ -0,0 +1,56 @@ +/* + * 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.cli.http; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link ClientFactory}. + */ +public class ClientFactoryTest +{ + + static Path keyStorePath() throws Exception + { + return Paths.get(ClientFactoryTest.class.getResource("/test-keystore.p12").toURI()); + } + + @Test + public void createsClientFromPKCS12Keystore() throws Exception + { + assertNotNull(ClientFactory.createClient(keyStorePath(), "changeit")); + } + + @Test + public void failsCleanlyOnWrongPassword() throws Exception + { + Path keyStore = keyStorePath(); + + assertThrows(IllegalArgumentException.class, () -> ClientFactory.createClient(keyStore, "wrong")); + } + + @Test + public void failsCleanlyOnMissingFile() + { + assertThrows(IllegalArgumentException.class, () -> ClientFactory.createClient(Path.of("/nonexistent.p12"), "changeit")); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java new file mode 100644 index 000000000..f7608273a --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java @@ -0,0 +1,85 @@ +/* + * 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.cli.sparql; + +import java.net.URI; +import org.apache.jena.query.Syntax; +import org.apache.jena.update.UpdateFactory; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link Updates}: every template must be valid standard SPARQL 1.1. + */ +public class UpdatesTest +{ + + private static final URI DOC = URI.create("https://localhost:4443/some/"); + private static final URI BASE = URI.create("https://localhost:4443/"); + private static final URI ADMIN_BASE = URI.create("https://admin.localhost:4443/"); + + @Test + public void insertOntologyImportIsValidSPARQL11() + { + String update = Updates.insertOntologyImport(DOC, URI.create("https://example.org/ontology#")); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + } + + @Test + public void insertGroupMemberIsValidSPARQL11() + { + String update = Updates.insertGroupMember(DOC, URI.create("https://localhost:4443/agents/x/#this")); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + } + + @Test + public void removeBlockWithoutBlockKeepsVariable() + { + String update = Updates.removeBlock(DOC, null); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("?block")); + } + + @Test + public void removeBlockWithBlockInjectsIRI() + { + String update = Updates.removeBlock(DOC, URI.create("https://localhost:4443/some/#block")); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + assertFalse(update.contains("?block")); + } + + @Test + public void makePublicIsValidSPARQL11() + { + String update = Updates.makePublic(BASE, ADMIN_BASE); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + assertTrue(update.contains("")); + assertTrue(update.contains("")); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java new file mode 100644 index 000000000..275030c71 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java @@ -0,0 +1,74 @@ +/* + * 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.cli.util; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests for {@link SequenceNumbers}. + */ +public class SequenceNumbersTest +{ + + private static final String DOC = "https://localhost:4443/some/"; + + @Test + public void returnsFirstMembershipPropertyOnEmptyModel() + { + Model model = ModelFactory.createDefaultModel(); + + assertEquals(RDF.li(1), SequenceNumbers.nextSequenceProperty(model, model.createResource(DOC))); + } + + @Test + public void returnsMaxPlusOneWithGaps() + { + Model model = ModelFactory.createDefaultModel(); + Resource doc = model.createResource(DOC); + doc.addProperty(RDF.li(1), model.createResource()); + doc.addProperty(RDF.li(2), model.createResource()); + doc.addProperty(RDF.li(7), model.createResource()); + + assertEquals(RDF.li(8), SequenceNumbers.nextSequenceProperty(model, doc)); + } + + @Test + public void ignoresOtherSubjects() + { + Model model = ModelFactory.createDefaultModel(); + model.createResource("https://localhost:4443/other/").addProperty(RDF.li(5), model.createResource()); + + assertEquals(RDF.li(1), SequenceNumbers.nextSequenceProperty(model, model.createResource(DOC))); + } + + @Test + public void ignoresNonNumericSuffixes() + { + Model model = ModelFactory.createDefaultModel(); + Resource doc = model.createResource(DOC); + doc.addProperty(model.createProperty(RDF.getURI() + "_x"), model.createResource()); + doc.addProperty(RDF.li(3), model.createResource()); + + assertEquals(RDF.li(4), SequenceNumbers.nextSequenceProperty(model, doc)); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.java new file mode 100644 index 000000000..222a90863 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.java @@ -0,0 +1,76 @@ +/* + * 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.cli.util; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link URIRewriter}. + */ +public class URIRewriterTest +{ + + @Test + public void rewriteReplacesOriginKeepingPathQueryFragment() + { + assertEquals(URI.create("https://localhost:8443/a%20b/c/?d=e#f"), + URIRewriter.rewrite(URI.create("https://linkeddatahub.com/a%20b/c/?d=e#f"), URI.create("https://localhost:8443"))); + } + + @Test + public void rewriteIgnoresProxyPath() + { + assertEquals(URI.create("https://localhost:8443/some/"), + URIRewriter.rewrite(URI.create("https://linkeddatahub.com:4443/some/"), URI.create("https://localhost:8443/ignored/"))); + } + + @Test + public void rewriteRejectsRelativeURI() + { + assertThrows(IllegalArgumentException.class, + () -> URIRewriter.rewrite(URI.create("/relative/path"), URI.create("https://localhost:8443"))); + } + + @Test + public void adminBasePrefixesHostWithAdminSubdomain() + { + assertEquals(URI.create("https://admin.localhost:4443/"), URIRewriter.adminBase(URI.create("https://localhost:4443/"))); + } + + @Test + public void encodeSlugKeepsUnreservedCharacters() + { + assertEquals("abc-._~123", URIRewriter.encodeSlug("abc-._~123")); + } + + @Test + public void encodeSlugEncodesReservedAndNonASCII() + { + assertEquals("a%20b%2F%C4%87", URIRewriter.encodeSlug("a b/ć")); + } + + @Test + public void childURIAppendsEncodedSlugAndSlash() + { + assertEquals(URI.create("https://localhost:4443/some/my%20item/"), + URIRewriter.childURI(URI.create("https://localhost:4443/some/"), "my item")); + } + +} diff --git a/cli/src/test/resources/test-keystore.p12 b/cli/src/test/resources/test-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f50715e253cfffbd40c78705aa68c0712ed6658c GIT binary patch literal 2772 zcma);X*d*&7RP7CFlKCH4;6((Vq{+?jBH~YvhPcieU~l5*v9gT?E6?ETgqO_R+1t_ zMn<-=P1%M>gtz|4X7R1mkFPf8)&Gb)SymKQ2ZX5JbSys{Mv4h}(Z0v`h#qgu*`~ zBjO5}qgVF)IoOnceL_72Z#JGYE7^)F1c5~0AfNz(k&gb~7eP>30OA}S)G!_Xh3OQ~}ItXxo%-!mjWU}ZY z_~AzVl zmzVwg*S(M=+q;&!4hMmH$r_?DJql{ei}J|nmmT%-tFt5+%A)=Gd*_H7uj0R^^Mo63 z0T(9%aumN8Yu8~=#I|G3k9P)^yu2K)>$bF$e~5cY?6`o)aB5{thiGw!tjl)q9^y`Y zV1;irT90lg75m&&W2VT(o;S*Y?Q?r_@T!c8IBhw6#k05q|3hIERKcDq^Z8f$&|9{6YIIpCN`mz z=HPFk`pzAW^m~fzI+%qgkM{K#B3CMA-e#(4L-}VA1SW}F?Nf6nPuE_aco54kCY-1$ zMC5Wg|CoF3tT1jkzRyu$w6M4NZQLIp+W=QyvHz^#x2=iQ7j~tuOk#RTIaqyxT$7}Y z_XIPFF5885$v2O;`V7d>2L}%Z@GTf|H9&XBkEg7%q<;*|B$?Me7<@uViS>_hlVre` zIukMNt#cQYSIrGM-#Tf*JJEeHvahCmHXjl?g^#!NrnFgizqx#ky^G{r?7Dg~!GR3_ zwEyx1)?jbIcX;odDU0-jP1{ymK%I$T77`=TDG_0A{_#linh#cQjeUTs<&N?r&T?F? zFS#38%a)fY_ntNRK;^yYha@?^V_Q`dc@sG;#c9ZE$PID14oS7UTj7WPf?&-(-R4F!03cAh{oj9i* zsQ#+q>FDlaBh!V^A0hU$NrP1jfmOLKLgiOG<*;{qc}PpEM%x(bSZst7D`~)IOoXP* zfZ+Gie^sT^jKCr1H5Y_DN#SdUwIY^T>2xG9Ng^TH!`C(5S$V4Ssdlp4-BcdTEJ4mg z?5Es4cqfJv8?j+%Tdy~{0^0hvJ4(0h&co@W<)OMV%G3@e#pCwWX5bX2B2O*gT%*He$5} znqe``w%|@!&eiFAs$$Q^_H+9zcia6HUzg)sWHQ_-p6>D7PD`{Yte8Sa%Vr+30d3&w ztC_7refWj%rQ}y{P^>jWuBw|{NG+yPP8Iso!<(fXJo6*0!NDbU?z`GdPiJ*wJP*ui z?q{1Wh(DS-b_y`4e^O)%l8yZW%_u|XUZWOmbZy8z-s?eEg$pQ&kHZ>Utc8aAZaI+! zzx4kqeO_#7+AiJVmfr|jJp3M&s*2Xm%#)Kf+U9q}bHbZymiOPHLDa7BcqUED}3 zTUvYJU~aS5;Mfgt|DYd+`DMw$q&`Yg8_Rzxr9VmMu~V`uCKHoju?9IyyWQA1(Kcin zEHIwHNYf^&4jHIvnhe09ZgEAP8~cAAwVf1I%XX?u`|Yl2?$Hk&}~`R*;vK zMc}|s|13f12sm*0Z&U&T0)DsLe-z;Vis;9cr=8kj`Pq2Z73c6cvf##kBisKI(TqN* zXRblfAUUGwoVa2gT-si19N|7+dBQ!(KX% zZK&mHS<#sYvl^SSx1pl0SfqTR+LvzB!>Zmg-Ez{_P zB(Yxe^Wts#Xnf3_l1%NS(BsFBv<2W~-!~;B=;mMIWdVW1)y3N3+CH@ndMS{sej&|U znHQ+T3h`fVV4}TS*!*z_|GFh!LTgn0Of0Zsl6pRVnNMTG0bEI{?B!T6T*c=85C4^Fq9%H`?Q# z%Y&3$C`B#sA|7RfT|Qd|EWT!1%lxr@`Cu96^(E#p;ySu~C@QhD)ij3q`#z~Bx z^G{!^blndl>`-G}*EUi(w1A=Wp+K|o>bdseB2Kn-8rL25R#l8L5GA?1t)JFcc!)G8 zpC3q*XV*37`DuT9sqij+hC=tbcN(qG{_DxocC~dLH;Rq@KY3T5;F+7nAk4W@p`Xuo z*=gn3SK4HKbhB(>w}~8^;I$o1g1wvDN)kdfGTl*5otDtUH{}bMO&xPs8F~{y$3yi- zso}4MsQbMy4nzgr%E7@>u!Gs@NvV7n~Y9Bz+61 z_r=9j;kc=zog{kfV&dPldgm_bOjpr0;>L^AtN~cnG;28HIG=9td&nDAS^}S>}c~R8<;|(OB5})_$A7JGkJ3C~{*;yzIE|0#=}phueYl zVPk+$_qM_))1$-pb(X!o<&Q%;!7AHJy7$=dnGe|nD{?A)gix(Ym?(bYJ+uy<9Y&U5 zappd7y+}(nOg1JfWc)C9x|p-!PLpArF3N(m`M7hKpEI{&gE<(+dj~nFPb3Nt20yU7 z6S722GYKim*6e=@3p@}KTC~_$Ujcs=MpF3sW1+d<-u&z^#SGHt8CFs_kB4QL@xvsR zYNf<&A``9g{T|28=+C1z=hk&bDVVtN425Q))38D39+(sRus-^}+jldn`R03r4yo1P zoW5tZM!}jvht~FEIB$}b$dijb-b_7B(r=oyC1?yK5C#Y-1k;~S4+NwIfCaea4Sa(t zhYE!rg)wrS=SYJ3>rJc{ja=BteHNDof_?>a1f2CNmHtTH-Sn{RQ=M#rD49J{{8vQ% E4T44ouK)l5 literal 0 HcmV?d00001