diff --git a/CHANGELOG.md b/CHANGELOG.md index 109d3ac07c..66d9c2c1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ ## [Unreleased] ### Changed -- Application ontologies resolved as a native ontapi `owl:imports` union graph (cached per ontology URI) instead of a manually flattened, RDFS-materialized model — no RDFS inference -- `Namespace` no-query GET serves the raw ontology graph from the shared repository instead of rebuilding a repository per request +- Application ontologies resolved as a native ontapi `owl:imports` union graph (cached per ontology URI), no RDFS inference — replaces the manually flattened, RDFS-materialized model +- `Namespace` no-query GET serves the raw ontology graph from the shared repository instead of rebuilding one per request +- **BREAKING**: "Add data" and "Generate containers" orchestrated client-side over the Graph Store Protocol (POST-append via `?uri=` proxy; per-class container PUT fan-out embedding the view as `ldh:Object` → `rdf:value` → `ldh:View`), replacing the `/add` and `/generate` endpoints ### Fixed -- Raw ontology graphs no longer leak inferred `rdf:type rdfs:Resource`, which produced multi-token `@typeof` that broke View block rendering +- Raw ontology graphs no longer leak inferred `rdf:type rdfs:Resource` that broke View block rendering via multi-token `@typeof` ### Removed -- The Linked Data proxy no longer serves ontology terms; it is now dumb transport (bundled-vocab file cache + SSRF-checked external fetch), with ontology terms served by `/ns` +- Linked Data proxy no longer serves ontology terms (now dumb transport: bundled-vocab file cache + SSRF-checked external fetch); ontology terms served by `/ns` +- **BREAKING**: `/add` and `/generate` server-side endpoints (`Add`/`Generate` JAX-RS resources), superseded by the client-orchestrated writes; removes their server-side fetch/SSRF surface (LNK-002); `/transform` retained until a client-side SPARQL engine lands ## [5.7.1] - 2026-08-06 ### Changed diff --git a/http-tests/add/GET-proxied-source-POST-append.sh b/http-tests/add/GET-proxied-source-POST-append.sh new file mode 100755 index 0000000000..5db813ac2c --- /dev/null +++ b/http-tests/add/GET-proxied-source-POST-append.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# Exercises the client-orchestrated "Add data" flow that replaced the server-side /add endpoint: +# the browser GETs the external source through the same-origin ?uri= proxy as RDF/XML, then +# POSTs (appends) it to the target document. Two requests, no /add endpoint. + +# add agent to the readers group (to read through the proxy) and the writers group (to append) + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/readers/" + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/writers/" + +# create the target container + +container=$(create-container.sh \ + -f "$AGENT_CERT_FILE" \ + -p "$AGENT_CERT_PWD" \ + -b "$END_USER_BASE_URL" \ + --title "Test" \ + --slug "test" \ + --parent "$END_USER_BASE_URL") + +# step 1: fetch the external source through the LDH proxy, converted to RDF/XML + +source_rdfxml=$(curl -k -f -s -G \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H "Accept: application/rdf+xml" \ + --data-urlencode "uri=https://orcid.org/0000-0003-1750-9906" \ + "$END_USER_BASE_URL") + +# step 2: append the fetched triples to the target container document (GSP append -> 204) + +echo "$source_rdfxml" | curl -k -w "%{http_code}\n" -o /dev/null -s \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/rdf+xml" \ + --data-binary @- \ + "$container" \ +| grep -q "$STATUS_NO_CONTENT" diff --git a/http-tests/add/POST-add.sh b/http-tests/add/POST-add.sh deleted file mode 100755 index dcca9c6d8f..0000000000 --- a/http-tests/add/POST-add.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" - -# add agent to the writers group - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# create container - -slug="test" - -container=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Test" \ - --slug "$slug" \ - --parent "$END_USER_BASE_URL") - -# import data into the container - -curl -w "%{http_code}\n" -o /dev/null -k -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${container}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_NO_CONTENT" diff --git a/http-tests/add/PUT-generate-container.sh b/http-tests/add/PUT-generate-container.sh new file mode 100755 index 0000000000..9500e29a1d --- /dev/null +++ b/http-tests/add/PUT-generate-container.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# Exercises the client-orchestrated "Generate containers" flow that replaced the server-side +# /generate endpoint. The client builds one container document per checked class -- a dh:Container +# whose content block is an ldh:Object wrapping an ldh:View over a $type-parameterized SELECT -- and +# PUTs it. This test PUTs one such container (shaped exactly like ldh:generate-container-doc output) +# and verifies: creation succeeds (the Object-wrapped block passes ldh:InvalidContentBlockType / +# MissingValue / MissingQuery validation), the server stamps metadata, and the block persists. + +# add agent to the writers group + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/writers/" + +parent="$END_USER_BASE_URL" +uuid=$(uuidgen | tr '[:upper:]' '[:lower:]') +container="${parent}${uuid}/" +class="https://www.w3.org/ns/ldt/document-hierarchy#Container" + +# PUT the generated container document (blank nodes are skolemized server-side) + +http_code=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X PUT \ + -H "Content-Type: application/rdf+xml" \ + --data-binary @- \ + "$container" < + + + + Containers + ${uuid} + + + + + + + + + + Select Container + SELECT DISTINCT ?s WHERE { ?s a <${class}> ; ?p ?o } + + + + + + + + +EOF +) + +[ "$http_code" = "$STATUS_CREATED" ] + +# fetch the created container and verify the shape + server-stamped metadata + +ntriples=$(curl -k -f -s \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H "Accept: application/n-triples" \ + "$container") + +# parent link, generated title, and server-stamped creation date +echo "$ntriples" | grep -q " <${parent}>" +echo "$ntriples" | grep -q " \"Containers\"" +echo "$ntriples" | grep -q "" + +# content block persisted as an ldh:Object, and the SELECT carries the substituted class IRI +echo "$ntriples" | grep -q "" +echo "$ntriples" | grep "" | grep -q "${class}" diff --git a/http-tests/system/end-user/POST-add-401.sh b/http-tests/system/end-user/POST-add-401.sh deleted file mode 100755 index bb927b19a5..0000000000 --- a/http-tests/system/end-user/POST-add-401.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# POST /add without a certificate should return 401 -# Only owners and writers have acl:Append access to /add via write-append authorization - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${END_USER_BASE_URL}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_UNAUTHORIZED" diff --git a/http-tests/system/end-user/POST-add-403.sh b/http-tests/system/end-user/POST-add-403.sh deleted file mode 100755 index 68d43ea7e1..0000000000 --- a/http-tests/system/end-user/POST-add-403.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# POST /add with a signed-up agent not in any group should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${END_USER_BASE_URL}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-add-readers-403.sh b/http-tests/system/end-user/POST-add-readers-403.sh deleted file mode 100755 index d512b3ee42..0000000000 --- a/http-tests/system/end-user/POST-add-readers-403.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# POST /add with a reader should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only; -# readers only have acl:Read on dh:Item/Container and /sparql, not on /add - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/readers/" - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${END_USER_BASE_URL}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-add.sh b/http-tests/system/end-user/POST-add.sh deleted file mode 100755 index 40f715c763..0000000000 --- a/http-tests/system/end-user/POST-add.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# add agent to the writers group - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# create container to hold the cloned data - -slug=$(uuidgen | tr '[:upper:]' '[:lower:]') - -container=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Test container" \ - --slug "$slug" \ - --parent "$END_USER_BASE_URL") - -# POST /add with a writer should succeed -# Clone data from a remote RDF source into the container - -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "rdf=" \ - --data-urlencode "sb=clone" \ - --data-urlencode "pu=http://purl.org/dc/terms/source" \ - --data-urlencode "ou=https://orcid.org/0000-0003-1750-9906" \ - --data-urlencode "pu=http://www.w3.org/ns/sparql-service-description#name" \ - --data-urlencode "ou=${container}" \ - "${END_USER_BASE_URL}add" \ -| grep -q "$STATUS_NO_CONTENT" diff --git a/http-tests/system/end-user/POST-generate-401.sh b/http-tests/system/end-user/POST-generate-401.sh deleted file mode 100755 index a99df03753..0000000000 --- a/http-tests/system/end-user/POST-generate-401.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# POST /generate without a certificate should return 401 -# Only owners and writers have acl:Append access to /generate via write-append authorization - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -[] sioc:has_parent <${END_USER_BASE_URL}> . -EOF -) \ -| grep -q "$STATUS_UNAUTHORIZED" diff --git a/http-tests/system/end-user/POST-generate-403.sh b/http-tests/system/end-user/POST-generate-403.sh deleted file mode 100755 index 70838dafb7..0000000000 --- a/http-tests/system/end-user/POST-generate-403.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# POST /generate with a signed-up agent not in any group should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -[] sioc:has_parent <${END_USER_BASE_URL}> . -EOF -) \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-generate-readers-403.sh b/http-tests/system/end-user/POST-generate-readers-403.sh deleted file mode 100755 index 751aeb8660..0000000000 --- a/http-tests/system/end-user/POST-generate-readers-403.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# POST /generate with a reader should return 403 -# The write-append authorization grants acl:Append to owners and writers groups only; -# readers only have acl:Read on dh:Item/Container and /sparql, not on /generate - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/readers/" - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -[] sioc:has_parent <${END_USER_BASE_URL}> . -EOF -) \ -| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/system/end-user/POST-generate.sh b/http-tests/system/end-user/POST-generate.sh deleted file mode 100755 index 715b671dba..0000000000 --- a/http-tests/system/end-user/POST-generate.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# add agent to the writers group - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# create a parent container to generate into - -slug=$(uuidgen | tr '[:upper:]' '[:lower:]') - -parent=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Generate parent" \ - --slug "$slug" \ - --parent "$END_USER_BASE_URL") - -# POST /generate with a writer: generate a container for dh:Container class using ldh:SelectChildren query - -( -curl -k -w "%{http_code}\n" -o /dev/null -s \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X POST \ - -H "Content-Type: text/turtle" \ - --data-binary @- \ - "${END_USER_BASE_URL}generate" < . -@prefix void: . -@prefix spin: . -@prefix dh: . -@prefix ldh: . - -[] sioc:has_parent <${parent}> ; - void:class dh:Container ; - spin:query ldh:SelectChildren . -EOF -) \ -| grep -q "$STATUS_OK" diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java deleted file mode 100644 index 9bc6b93ad2..0000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright 2021 Martynas Jusevičius - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.atomgraph.linkeddatahub.resource; - -import com.atomgraph.core.MediaTypes; -import com.atomgraph.core.vocabulary.SD; -import com.atomgraph.linkeddatahub.client.GraphStoreClient; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.Optional; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.StreamingOutput; -import jakarta.ws.rs.core.UriInfo; -import jakarta.ws.rs.ext.Providers; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ResIterator; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.DCTerms; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS endpoint for adding RDF data. - * - * @author {@literal Martynas Jusevičius } - */ -public class Add -{ - - private static final Logger log = LoggerFactory.getLogger(Add.class); - - private final UriInfo uriInfo; - private final MediaTypes mediaTypes; - private final Optional agentContext; - private final com.atomgraph.linkeddatahub.Application system; - - /** - * Constructs endpoint for synchronous RDF data imports. - * - * @param request current request - * @param uriInfo current URI info - * @param mediaTypes supported media types - * @param providers JAX-RS providers - * @param system system application - * @param agentContext authenticated agent's context - */ - @Inject - public Add(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - Optional agentContext, - @Context Providers providers, com.atomgraph.linkeddatahub.Application system) - { - this.uriInfo = uriInfo; - this.mediaTypes = mediaTypes; - this.agentContext = agentContext; - this.system = system; - } - - /** - * Adds RDF data from a remote source to a named graph. - * Expects a model containing a resource with dct:source (source URI) and sd:name (target graph URI) properties. - * - * @param model the RDF model containing the import parameters - * @return JAX-RS response with the imported data - */ - @POST - public Response post(Model model) - { - ResIterator it = model.listSubjectsWithProperty(DCTerms.source); - try - { - if (!it.hasNext()) throw new BadRequestException("Argument resource not provided"); - - Resource arg = it.next(); - Resource source = arg.getPropertyResourceValue(DCTerms.source); - if (source == null) throw new BadRequestException("RDF source URI (dct:source) not provided"); - - Resource graph = arg.getPropertyResourceValue(SD.name); - if (graph == null || !graph.isURIResource()) throw new BadRequestException("Graph URI (sd:name) not provided"); - - GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getMediaTypes()); // TO-DO: inject - Model importModel = gsc.getModel(source.getURI()); - // forward the stream to the named graph document -- do not directly append triples to graph because the agent might not have access to it - return forwardPost(Entity.entity(importModel, com.atomgraph.client.MediaType.APPLICATION_NTRIPLES_TYPE), graph.getURI()); - } - finally - { - it.close(); - } - } - - /** - * Forwards POST request to a graph. - * - * @param entity request entity - * @param graphURI the graph URI - * @return JAX-RS response - */ - protected Response forwardPost(Entity entity, String graphURI) - { - GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()). - delegation(getUriInfo().getBaseUri(), getAgentContext().orElse(null)); - // forward the stream to the named graph document. Buffer the entity first so that the server response is not returned before the client response completes - try (Response response = gsc.post(URI.create(graphURI), entity, gsc.getReadableMediaTypes(Model.class))) - { - return Response.status(response.getStatus()). - entity(response.readEntity(Model.class)). - build(); - } - } - - /** - * Converts input stream to streaming output. - * @param is input stream - * @return streaming output - */ - public StreamingOutput getStreamingOutput(InputStream is) - { - return (OutputStream os) -> { - is.transferTo(os); - }; - } - - /** - * Returns the supported media types. - * - * @return media types - */ - public MediaTypes getMediaTypes() - { - return mediaTypes; - } - - /** - * Returns the current URI info. - * - * @return URI info - */ - public UriInfo getUriInfo() - { - return uriInfo; - } - - /** - * Returns the authenticated agent's context. - * - * @return optional agent context - */ - public Optional getAgentContext() - { - return agentContext; - } - - /** - * Returns the system application. - * - * @return system application - */ - public com.atomgraph.linkeddatahub.Application getSystem() - { - return system; - } - -} diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java deleted file mode 100644 index 27e2124ffc..0000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java +++ /dev/null @@ -1,313 +0,0 @@ -/** - * Copyright 2022 Martynas Jusevičius - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.atomgraph.linkeddatahub.resource; - -import com.atomgraph.core.MediaTypes; -import com.atomgraph.linkeddatahub.apps.model.Application; -import com.atomgraph.linkeddatahub.server.model.impl.DocumentHierarchyGraphStoreImpl; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import com.atomgraph.linkeddatahub.server.util.Skolemizer; -import com.atomgraph.linkeddatahub.vocabulary.LDH; -import com.atomgraph.linkeddatahub.vocabulary.VoID; -import com.atomgraph.linkeddatahub.vocabulary.DH; -import com.atomgraph.linkeddatahub.vocabulary.SIOC; -import com.atomgraph.spinrdf.vocabulary.SP; -import com.atomgraph.spinrdf.vocabulary.SPIN; -import java.net.URI; -import java.util.Calendar; -import java.util.Optional; -import java.util.UUID; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.InternalServerErrorException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.container.ResourceContext; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriBuilder; -import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.ontapi.model.OntModel; -import org.apache.jena.query.ParameterizedSparqlString; -import org.apache.jena.query.Query; -import org.apache.jena.query.QueryFactory; -import org.apache.jena.query.Syntax; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.ResIterator; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.DCTerms; -import org.apache.jena.vocabulary.RDF; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS resource that generates containers for given classes. - * - * @author {@literal Martynas Jusevičius } - */ -public class Generate -{ - - private static final Logger log = LoggerFactory.getLogger(Generate.class); - - private final UriInfo uriInfo; - private final MediaTypes mediaTypes; - private final Application application; - private final OntModel ontology; - private final Optional agentContext; - private final com.atomgraph.linkeddatahub.Application system; - private final ResourceContext resourceContext; - - /** - * Constructs endpoint for container generation. - * - * @param request current request - * @param uriInfo current URI info - * @param mediaTypes supported media types - * @param application matched application - * @param ontology ontology of the current application - * @param system system application - * @param agentContext authenticated agent's context - * @param resourceContext resource context for creating resources - */ - @Inject - public Generate(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional agentContext, - com.atomgraph.linkeddatahub.Application system, @Context ResourceContext resourceContext) - { - if (ontology.isEmpty()) throw new InternalServerErrorException("Ontology is not specified"); - this.uriInfo = uriInfo; - this.mediaTypes = mediaTypes; - this.application = application; - this.ontology = ontology.get(); - this.agentContext = agentContext; - this.system = system; - this.resourceContext = resourceContext; - } - - /** - * Generates containers for given classes. - * Expects a model containing a parent container (sioc:has_parent) and one or more class specifications - * with void:class and spin:query properties. Creates a new container for each class with a view based - * on the provided SPARQL SELECT query. - * - * @param model the RDF model containing the generation parameters - * @return JAX-RS response indicating success or failure - */ - @POST - public Response post(Model model) - { - ResIterator it = model.listSubjectsWithProperty(SIOC.HAS_PARENT); - try - { - if (!it.hasNext()) throw new BadRequestException("Argument resource not provided"); - - Resource arg = it.next(); - Resource service = arg.getPropertyResourceValue(LDH.service); - Resource parent = arg.getPropertyResourceValue(SIOC.HAS_PARENT); - if (parent == null) throw new BadRequestException("Parent container (sioc:has_parent) not provided"); - - ResIterator partIt = model.listSubjectsWithProperty(VoID._class); - try - { - while (partIt.hasNext()) - { - Resource part = partIt.next(); - Resource cls = part.getPropertyResourceValue(VoID._class); - Resource queryRes = part.getPropertyResourceValue(SPIN.query); - if (queryRes == null) throw new BadRequestException("Container query string (spin:query) not provided"); - - // Lookup query in ontology - Resource queryResource = getOntology().getResource(queryRes.getURI()); - if (queryResource == null || !queryResource.hasProperty(SP.text)) - throw new BadRequestException("Query resource not found in ontology: " + queryRes.getURI()); - - String queryString = queryResource.getProperty(SP.text).getString(); - Query query = QueryFactory.create(queryString, Syntax.syntaxARQ); - if (!query.isSelectType()) throw new BadRequestException("Container query is not of SELECT type"); - - ParameterizedSparqlString pss = new ParameterizedSparqlString(query.toString()); - pss.setIri(RDF.type.getLocalName(), cls.getURI()); // inject $type value - - URI containerGraphURI = UriBuilder.fromUri(parent.getURI()).path("{slug}/").build(UUID.randomUUID().toString()); - Model containerModel = ModelFactory.createDefaultModel(); - - createContainer(containerModel, - containerGraphURI, parent, - cls.getLocalName() + "s", - createView(containerModel, createContainerSelect(containerModel, - "Select " + cls.getLocalName(), - pss.asQuery(), - service))); - new Skolemizer(containerGraphURI.toString()).apply(containerModel); - - // append triples directly to the graph store without doing an HTTP request (and thus no ACL check) - try (Response containerResponse = getResourceContext().getResource(DocumentHierarchyGraphStoreImpl.class).post(containerModel, false, containerGraphURI)) - { - if (!containerResponse.getStatusInfo().getFamily().equals(Status.Family.SUCCESSFUL)) - { - if (log.isErrorEnabled()) log.error("Cannot create container"); - throw new InternalServerErrorException("Cannot create container"); - } - } - } - } - finally - { - partIt.close(); - } - - // ban the parent container URI from proxy cache to make sure the next query using it will be fresh (e.g. SELECT that loads children) - getSystem().ban(getSystem().getServiceContext(getApplication().getService()).getBackendProxy(), parent.getURI(), true); - - return Response.ok().build(); - } - finally - { - it.close(); - } - } - - /** - * Creates SELECT SPARQL query. - * - * @param model RDF model - * @param title query title - * @param query query object - * @param service optional SPARQL service resource - * @return query resource - */ - public Resource createContainerSelect(Model model, String title, Query query, Resource service) - { - Resource resource = model.createResource(). - addProperty(RDF.type, SP.Select). - addLiteral(DCTerms.title, title). - addProperty(SP.text, query.toString()); - - if (service != null) resource.addProperty(LDH.service, service); - - return resource; - } - - /** - * Creates a container document. - * - * @param model RDF model - * @param graphURI named graph URI - * @param parent parent document resource - * @param title document title - * @param content document content - * @return container resource - */ - public Resource createContainer(Model model, URI graphURI, Resource parent, String title, Resource content) - { - return model.createResource(graphURI.toString()). - addProperty(RDF.type, DH.Container). - addProperty(SIOC.HAS_PARENT, parent). - addLiteral(DCTerms.title, title). - addLiteral(DH.slug, UUID.randomUUID().toString()). - addLiteral(DCTerms.created, Calendar.getInstance()). - addProperty(model.createProperty(RDF.getURI(), "_1"), content); // TO-DO: make sure we're creating sequence value larger than the existing ones? - } - - /** - * Creates content resource. - * - * @param model RDF model - * @param query query resource - * @return content resource - */ - public Resource createView(Model model, Resource query) - { - return model.createResource(). - addProperty(RDF.type, LDH.View). - addProperty(SPIN.query, query); - } - - /** - * Returns the supported media types. - * - * @return media types - */ - public MediaTypes getMediaTypes() - { - return mediaTypes; - } - - /** - * Returns the current application. - * - * @return the application - */ - public Application getApplication() - { - return application; - } - - /** - * Returns the ontology. - * - * @return the ontology - */ - public OntModel getOntology() - { - return ontology; - } - - /** - * Returns the current URI info. - * - * @return URI info - */ - public UriInfo getUriInfo() - { - return uriInfo; - } - - /** - * Returns the authenticated agent's context. - * - * @return optional agent context - */ - public Optional getAgentContext() - { - return agentContext; - } - - /** - * Returns the system application. - * - * @return system application - */ - public com.atomgraph.linkeddatahub.Application getSystem() - { - return system; - } - - /** - * Returns the resource context. - * - * @return resource context - */ - public ResourceContext getResourceContext() - { - return resourceContext; - } - -} \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java index 670c8a0d21..bfbc2562bb 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java @@ -16,8 +16,6 @@ */ package com.atomgraph.linkeddatahub.server.model.impl; -import com.atomgraph.linkeddatahub.resource.Add; -import com.atomgraph.linkeddatahub.resource.Generate; import com.atomgraph.linkeddatahub.resource.Namespace; import com.atomgraph.linkeddatahub.resource.Transform; import com.atomgraph.linkeddatahub.resource.admin.ClearOntology; @@ -134,17 +132,6 @@ public Class getFileItem() return com.atomgraph.linkeddatahub.resource.upload.Item.class; } - /** - * Returns the endpoint for synchronous RDF imports. - * - * @return endpoint resource - */ - @Path("add") - public Class getAddEndpoint() - { - return Add.class; - } - /** * Returns the endpoint for synchronous RDF imports with a CONSTRUCT query transformation. * @@ -156,17 +143,6 @@ public Class getTransformEndpoint() return Transform.class; } - /** - * Returns the endpoint for container generation. - * - * @return endpoint resource - */ - @Path("generate") - public Class getGenerateEndpoint() - { - return Generate.class; - } - /** * Returns the endpoint that allows clearing ontologies from cache by URI. * diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl index 82dc364e41..928169c8a1 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl @@ -40,6 +40,9 @@ xmlns:ldt="&ldt;" xmlns:sd="&sd;" xmlns:sioc="&sioc;" xmlns:dct="&dct;" +xmlns:dh="&dh;" +xmlns:sp="&sp;" +xmlns:spin="&spin;" xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" extension-element-prefixes="ixsl" exclude-result-prefixes="#all" @@ -82,7 +85,7 @@ LIMIT 10 - + @@ -101,7 +104,10 @@ LIMIT 10 @@ -210,9 +216,7 @@ LIMIT 10 - -