From 2ecc5310caaf2402d31040bba074a3dd924601a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 12 Dec 2025 15:37:42 +0100 Subject: [PATCH 01/12] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7c0b2d1ed..f63745991 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.1.0 + 5.1.1-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub From 864183eda5de8442525f1b32ba3ea0297f193ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 12 Dec 2025 23:24:15 +0100 Subject: [PATCH 02/12] Simplified client-side refresh after OAuth login --- .../com/atomgraph/linkeddatahub/xsl/client.xsl | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) 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 3a615bdfa..e3f35cd31 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -296,22 +296,8 @@ WHERE - - - - - - + + From 27bf9cee7f7ec930d8ac9647f07288abc0a5116c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 14 Dec 2025 11:44:36 +0100 Subject: [PATCH 03/12] Make sure proxy passes on the response status --- README.md | 2 +- http-tests/proxy/GET-proxied-403.sh | 36 ++++++++ .../server/model/impl/ProxyResourceBase.java | 82 ++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100755 http-tests/proxy/GET-proxied-403.sh diff --git a/README.md b/README.md index 18ee07e3e..11a2c4642 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ The options are described in more detail in the [configuration documentation](ht If you need to start fresh and wipe the existing setup (e.g. after configuring a new base URI), you can do that using ```shell - sudo rm -rf data uploads && docker-compose down -v + sudo rm -rf fuseki uploads ssl datasets && docker-compose down -v ``` _:warning: This will **remove the persisted data and files** as well as Docker volumes._ diff --git a/http-tests/proxy/GET-proxied-403.sh b/http-tests/proxy/GET-proxied-403.sh new file mode 100755 index 000000000..04eadb652 --- /dev/null +++ b/http-tests/proxy/GET-proxied-403.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# add agent to the readers group to be able to read documents + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/readers/" + +# Test that status codes are correctly proxied through +# Generate a random UUID for a non-existing resource +random_uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen) +non_existing_uri="${END_USER_BASE_URL}${random_uuid}/" + +# Attempt to proxy a non-existing document on the END_USER_BASE_URL +# This should return 403 Forbidden (not found resources return 403 in LinkedDataHub) +http_status=$(curl -k -s -o /dev/null -w "%{http_code}" \ + -G \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H 'Accept: application/n-triples' \ + --data-urlencode "uri=${non_existing_uri}" \ + "$END_USER_BASE_URL" || true) + +# Verify that the proxied status code matches the backend status code (403) +if [ "$http_status" != "403" ]; then + echo "Expected HTTP 403 Forbidden for non-existing proxied document, got: $http_status" + exit 1 +fi diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java index 420a78a95..b590e424a 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java @@ -18,8 +18,11 @@ import com.atomgraph.client.MediaTypes; import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.HTMLMediaTypePredicate; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.core.exception.BadGatewayException; +import com.atomgraph.core.util.ModelUtils; +import com.atomgraph.core.util.ResultSetUtils; import com.atomgraph.linkeddatahub.apps.model.Dataset; import com.atomgraph.linkeddatahub.client.LinkedDataClient; import com.atomgraph.linkeddatahub.client.filter.auth.IDTokenDelegationFilter; @@ -52,15 +55,22 @@ import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.StatusType; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.core.Variant; import jakarta.ws.rs.ext.Providers; import org.apache.jena.query.ResultSet; +import org.apache.jena.query.ResultSetRewindable; import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFLanguages; +import org.apache.jena.riot.resultset.ResultSetReaderRegistry; import org.apache.jena.util.FileManager; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException; @@ -207,9 +217,77 @@ public Invocation.Builder getBuilder(WebTarget target) @Override public Response getResponse(Response clientResponse) { - if (clientResponse.getMediaType() == null) return Response.status(clientResponse.getStatus()).build(); + return getResponse(clientResponse, clientResponse.getStatusInfo()); + } + + public Response getResponse(Response clientResponse, StatusType statusType) + { + MediaType formatType = new MediaType(clientResponse.getMediaType().getType(), clientResponse.getMediaType().getSubtype()); // discard charset param + Lang lang = RDFLanguages.contentTypeToLang(formatType.toString()); + + // check if we got SPARQL results first + if (lang != null && ResultSetReaderRegistry.isRegistered(lang)) + { + ResultSetRewindable results = clientResponse.readEntity(ResultSetRewindable.class); + return getResponse(results, statusType); + } + + // fallback to RDF graph + Model description = clientResponse.readEntity(Model.class); + return getResponse(description, statusType); + } + + /** + * Returns response for the given RDF model. + * TO-DO: move down to Web-Client + * + * @param model RDF model + * @param statusType response status + * @return response object + */ + public Response getResponse(Model model, StatusType statusType) + { + List variants = com.atomgraph.core.model.impl.Response.getVariants(getWritableMediaTypes(Model.class), + getLanguages(), + getEncodings()); + + return new com.atomgraph.core.model.impl.Response(getRequest(), + model, + null, + new EntityTag(Long.toHexString(ModelUtils.hashModel(model))), + variants, + new HTMLMediaTypePredicate()). + getResponseBuilder(). + status(statusType). + build(); + } + + /** + * Returns response for the given SPARQL results. + * TO-DO: move down to Web-Client + * + * @param resultSet SPARQL results + * @param statusType response status + * @return response object + */ + public Response getResponse(ResultSetRewindable resultSet, StatusType statusType) + { + long hash = ResultSetUtils.hashResultSet(resultSet); + resultSet.reset(); + + List variants = com.atomgraph.core.model.impl.Response.getVariants(getWritableMediaTypes(ResultSet.class), + getLanguages(), + getEncodings()); - return super.getResponse(clientResponse); + return new com.atomgraph.core.model.impl.Response(getRequest(), + resultSet, + null, + new EntityTag(Long.toHexString(hash)), + variants, + new HTMLMediaTypePredicate()). + getResponseBuilder(). + status(statusType). + build(); } /** From 21d2ab088448dba8163c2176f5095c18628ffe48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 14 Dec 2025 12:12:38 +0100 Subject: [PATCH 04/12] NPE fix --- http-tests/proxy/POST-proxied-form.sh | 2 +- .../linkeddatahub/server/model/impl/ProxyResourceBase.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/http-tests/proxy/POST-proxied-form.sh b/http-tests/proxy/POST-proxied-form.sh index 9bfec095d..167bd1f72 100755 --- a/http-tests/proxy/POST-proxied-form.sh +++ b/http-tests/proxy/POST-proxied-form.sh @@ -19,7 +19,7 @@ add-agent-to-group.sh \ curl -k -w "%{http_code}\n" -o /dev/null -f -s \ -X POST \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/rdf+xml' \ --url-query "uri=${ADMIN_BASE_URL}clear" \ diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java index b590e424a..a1cc2f4dd 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java @@ -217,6 +217,8 @@ public Invocation.Builder getBuilder(WebTarget target) @Override public Response getResponse(Response clientResponse) { + if (clientResponse.getMediaType() == null) return Response.status(clientResponse.getStatus()).build(); + return getResponse(clientResponse, clientResponse.getStatusInfo()); } @@ -224,7 +226,7 @@ public Response getResponse(Response clientResponse, StatusType statusType) { MediaType formatType = new MediaType(clientResponse.getMediaType().getType(), clientResponse.getMediaType().getSubtype()); // discard charset param Lang lang = RDFLanguages.contentTypeToLang(formatType.toString()); - + // check if we got SPARQL results first if (lang != null && ResultSetReaderRegistry.isRegistered(lang)) { From a7399f3cdad78d5e4a3166d4d30f50b791ffa287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sun, 21 Dec 2025 00:18:15 +0200 Subject: [PATCH 05/12] `ldh:origin` renamed to `lapp:origin` --- config/system.trig | 4 +- http-tests/config/system.trig | 8 +- platform/select-root-services.rq | 4 +- .../atomgraph/linkeddatahub/Application.java | 4 +- .../apps/model/impl/ApplicationImpl.java | 3 +- .../linkeddatahub/vocabulary/LAPP.java | 3 + .../linkeddatahub/vocabulary/LDH.java | 3 - .../linkeddatahub/writer/XSLTWriterBase.java | 2 +- .../com/atomgraph/linkeddatahub/lapp.ttl | 82 ++++++++----------- .../xsl/bootstrap/2.3.2/client/functions.xsl | 6 +- .../xsl/bootstrap/2.3.2/client/modal.xsl | 8 +- .../xsl/bootstrap/2.3.2/layout.xsl | 38 ++++----- .../atomgraph/linkeddatahub/xsl/client.xsl | 6 +- 13 files changed, 81 insertions(+), 90 deletions(-) diff --git a/config/system.trig b/config/system.trig index 647f582c7..48e43ed77 100644 --- a/config/system.trig +++ b/config/system.trig @@ -18,7 +18,7 @@ a lapp:Application, lapp:AdminApplication ; dct:title "LinkedDataHub admin" ; # ldt:base ; - ldh:origin ; + lapp:origin ; ldt:ontology ; ldt:service ; ac:stylesheet ; @@ -38,7 +38,7 @@ a lapp:Application, lapp:EndUserApplication ; dct:title "LinkedDataHub" ; # ldt:base ; - ldh:origin ; + lapp:origin ; ldt:ontology ; ldt:service ; lapp:adminApplication ; diff --git a/http-tests/config/system.trig b/http-tests/config/system.trig index 47ed5c76a..a5d0e33e7 100644 --- a/http-tests/config/system.trig +++ b/http-tests/config/system.trig @@ -18,7 +18,7 @@ a lapp:Application, lapp:AdminApplication ; dct:title "LinkedDataHub admin" ; # ldt:base ; - ldh:origin ; + lapp:origin ; ldt:ontology ; ldt:service ; ac:stylesheet ; @@ -38,7 +38,7 @@ a lapp:Application, lapp:EndUserApplication ; dct:title "LinkedDataHub" ; # ldt:base ; - ldh:origin ; + lapp:origin ; ldt:ontology ; ldt:service ; lapp:adminApplication ; @@ -57,7 +57,7 @@ a lapp:Application, lapp:AdminApplication ; dct:title "Test admin" ; - ldh:origin ; + lapp:origin ; ldt:ontology ; ldt:service ; ac:stylesheet ; @@ -76,7 +76,7 @@ a lapp:Application, lapp:EndUserApplication ; dct:title "Test" ; - ldh:origin ; + lapp:origin ; ldt:ontology ; ldt:service ; lapp:adminApplication ; diff --git a/platform/select-root-services.rq b/platform/select-root-services.rq index 2a307e4e1..4c6d67546 100644 --- a/platform/select-root-services.rq +++ b/platform/select-root-services.rq @@ -7,11 +7,11 @@ PREFIX foaf: SELECT ?endUserApp ?endUserOrigin ?endUserQuadStore ?endUserEndpoint ?endUserAuthUser ?endUserAuthPwd ?endUserMaker ?adminApp ?adminOrigin ?adminQuadStore ?adminEndpoint ?adminAuthUser ?adminAuthPwd ?adminMaker { - ?endUserApp ldh:origin ?endUserOrigin ; + ?endUserApp lapp:origin ?endUserOrigin ; ldt:service ?endUserService ; lapp:adminApplication ?adminApp . ?adminApp ldt:service ?adminService ; - ldh:origin ?adminOrigin . + lapp:origin ?adminOrigin . ?endUserService a:quadStore ?endUserQuadStore ; sd:endpoint ?endUserEndpoint . ?adminService a:quadStore ?adminQuadStore ; diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index eeebe124a..986c5e3c7 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -1309,9 +1309,9 @@ public Resource getAppByOrigin(Model model, Resource type, URI absolutePath) Resource app = it.next(); // Use origin-based matching - return immediately on match since origins are unique - if (app.hasProperty(LDH.origin)) + if (app.hasProperty(LAPP.origin)) { - URI appOriginURI = URI.create(app.getPropertyResourceValue(LDH.origin).getURI()); + URI appOriginURI = URI.create(app.getPropertyResourceValue(LAPP.origin).getURI()); String normalizedAppOrigin = normalizeOrigin(appOriginURI); if (requestOrigin.equals(normalizedAppOrigin)) return app; diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java index 4a0c956f8..043b18d90 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java @@ -21,7 +21,6 @@ import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.vocabulary.FOAF; import com.atomgraph.linkeddatahub.vocabulary.LAPP; -import com.atomgraph.linkeddatahub.vocabulary.LDH; import com.atomgraph.server.vocabulary.LDT; import jakarta.ws.rs.core.UriBuilder; import org.apache.jena.enhanced.EnhGraph; @@ -69,7 +68,7 @@ public URI getBaseURI() @Override public Resource getOrigin() { - return getPropertyResourceValue(LDH.origin); + return getPropertyResourceValue(LAPP.origin); } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java index 6e0c49a11..fe51bdd86 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java @@ -82,4 +82,7 @@ public static String getURI() /** Read-only property */ public static final DatatypeProperty allowRead = m_model.createDatatypeProperty( NS + "allowRead" ); + /** Origin property for subdomain-based application matching */ + public static final ObjectProperty origin = m_model.createObjectProperty(NS + "origin"); + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java index cea639a6a..555ffdce5 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java @@ -101,9 +101,6 @@ public static String getURI() /** Service property */ public static final ObjectProperty service = m_model.createObjectProperty( NS + "service" ); - /** Origin property for subdomain-based application matching */ - public static final ObjectProperty origin = m_model.createObjectProperty( NS + "origin" ); - /** * For shape property */ public static final ObjectProperty forShape = m_model.createObjectProperty( NS + "forShape" ); diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java index 89b574dfb..49ff9130e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java @@ -138,7 +138,7 @@ public Map getParameters(MultivaluedMap", app); params.put(new QName("ldt", LDT.base.getNameSpace(), LDT.base.getLocalName()), new XdmAtomicValue(app.getBaseURI())); - params.put(new QName("ldh", LDH.origin.getNameSpace(), LDH.origin.getLocalName()), new XdmAtomicValue(app.getOriginURI())); + params.put(new QName("lapp", LAPP.origin.getNameSpace(), LAPP.origin.getLocalName()), new XdmAtomicValue(app.getOriginURI())); params.put(new QName("ldt", LDT.ontology.getNameSpace(), LDT.ontology.getLocalName()), new XdmAtomicValue(URI.create(app.getOntology().getURI()))); params.put(new QName("lapp", LAPP.Application.getNameSpace(), LAPP.Application.getLocalName()), getXsltExecutable().getProcessor().newDocumentBuilder().build(getSource(getAppModel(app, true)))); diff --git a/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl b/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl index 81402fb3d..77c6c15fe 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl @@ -82,22 +82,22 @@ :Application a rdfs:Class, owl:Class ; rdfs:subClassOf ldt:Application ; spin:constructor :ApplicationConstructor ; - spin:constraint :OneBasePerHostName, :StartsWithHTTPS, + spin:constraint :StartsWithHTTPS, :ValidOrigin, [ a ldh:MissingPropertyValue ; - rdfs:label "Missing base URI" ; - sp:arg1 ldt:base + rdfs:label "Missing origin URI" ; + sp:arg1 :origin ] ; rdfs:label "Application" ; - rdfs:comment "An application represents a data space identified by its base URI, in which application resource URIs are relative to the base URI. The only application interface (API) is read-write RESTful Linked data, backed by an RDF dataset accessible as a SPARQL 1.1 service. Application structure is defined in an ontology, which can import other ontologies." ; + rdfs:comment "An application represents a data space identified by its origin URI, in which application resource URIs are relative to the origin URI. The only application interface (API) is read-write RESTful Linked data, backed by an RDF dataset accessible as a SPARQL 1.1 service. Application structure is defined in an ontology, which can import other ontologies." ; rdfs:isDefinedBy : . :ApplicationConstructor a ldh:Constructor ; sp:text """ - PREFIX ldt: + PREFIX lapp: PREFIX rdfs: CONSTRUCT { - $this ldt:base [ a rdfs:Resource ] . + $this lapp:origin [ a rdfs:Resource ] . } WHERE { }""" ; @@ -166,47 +166,28 @@ # CONSTRAINTS :StartsWithHTTPS a sp:Construct ; - rdfs:label "ldt:base starts with https://" ; - sp:text """ -PREFIX ldt: -PREFIX spin: -PREFIX rdfs: - -CONSTRUCT { - _:c0 a spin:ConstraintViolation . - _:c0 spin:violationRoot ?this . - _:c0 spin:violationPath ldt:base . - _:c0 rdfs:label "Application base URI must start with https://" . -} -WHERE { - ?this ldt:base ?base - FILTER ( ! strstarts(str(?base), "https://") ) -}""" ; - rdfs:isDefinedBy : . - -:BasePathMatchesRegex a sp:Construct ; - rdfs:label "Base URI path does not match regex" ; + rdfs:label "lapp:origin starts with https://" ; sp:text """ -PREFIX ldt: +PREFIX lapp: PREFIX spin: PREFIX rdfs: CONSTRUCT { _:c0 a spin:ConstraintViolation . _:c0 spin:violationRoot ?this . - _:c0 spin:violationPath ldt:base . + _:c0 spin:violationPath lapp:origin . + _:c0 rdfs:label "Application origin URI must start with https://" . } WHERE { - ?this ldt:base ?base - BIND(strafter(strafter(str(?base), "//"), "/") AS ?path) - FILTER (!regex(?path, ?arg1, "i")) + ?this lapp:origin ?origin + FILTER ( ! strstarts(str(?origin), "https://") ) }""" ; rdfs:isDefinedBy : . -:OneBasePerHostName a sp:Construct ; # TO-DO: turn into spin:Template - rdfs:label "One ldt:base per hostname" ; +:ValidOrigin a sp:Construct ; + rdfs:label "Origin must not have trailing slash and must have hostname" ; sp:text """ -PREFIX ldt: +PREFIX lapp: PREFIX spin: PREFIX rdfs: @@ -214,18 +195,27 @@ CONSTRUCT { _:c0 a spin:ConstraintViolation . _:c0 spin:violationRoot ?this . - _:c0 spin:violationPath ldt:base . - _:c0 rdfs:label "Only one base per hostname is allowed" . + _:c0 spin:violationPath lapp:origin . + _:c0 rdfs:label ?message . } WHERE - { ?this ldt:base ?base - { SELECT ?this - WHERE - { ?this ldt:base ?base - BIND(strbefore(strafter(str(?base), "//"), "/") AS ?hostname) - } - GROUP BY ?this ?hostname - HAVING ( COUNT(?hostname) > 1 ) - } - }""" ; + { + ?this lapp:origin ?origin . + + BIND(STRAFTER(str(?origin), "//") AS ?afterScheme) + BIND(IF(CONTAINS(?afterScheme, "/"), + STRBEFORE(?afterScheme, "/"), + ?afterScheme) AS ?hostname) + + BIND( + IF(STRENDS(str(?origin), "/"), + "Origin URL must not end with a trailing slash", + IF(?hostname = "", + "Origin URL must have a non-empty hostname", + "")) AS ?message + ) + + FILTER(?message != "") + } +""" ; rdfs:isDefinedBy : . \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl index 5f67d49a4..a411b137b 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl @@ -1,5 +1,6 @@ @@ -22,6 +23,7 @@ xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:json="http://www.w3.org/2005/xpath-functions" xmlns:array="http://www.w3.org/2005/xpath-functions/array" xmlns:fn="http://www.w3.org/2005/xpath-functions" +xmlns:lapp="&lapp;" xmlns:ac="∾" xmlns:ldh="&ldh;" xmlns:rdf="&rdf;" @@ -53,7 +55,7 @@ exclude-result-prefixes="#all" - + @@ -61,7 +63,7 @@ exclude-result-prefixes="#all" - + 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 ea021a509..4f63577a7 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 @@ -1,5 +1,6 @@ @@ -29,6 +30,7 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:json="http://www.w3.org/2005/xpath-functions" xmlns:array="http://www.w3.org/2005/xpath-functions/array" +xmlns:lapp="&lapp;" xmlns:ac="∾" xmlns:ldh="&ldh;" xmlns:rdf="&rdf;" @@ -344,7 +346,7 @@ LIMIT 10 - + @@ -414,7 +416,7 @@ LIMIT 10 - +
URL-based access @@ -714,7 +716,7 @@ LIMIT 10 - + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl index 0f2909839..8b8f21876 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl @@ -100,7 +100,7 @@ exclude-result-prefixes="#all"> - + @@ -163,15 +163,13 @@ LIMIT 100 ; - ?base + { { ?app ?origin } UNION - { ?resource a ; - ?endpoint + { ?service ?endpoint } } } @@ -272,9 +270,9 @@ LIMIT 100 - <xsl:if test="$lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin]"> + <xsl:if test="$lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin]"> <xsl:value-of> - <xsl:apply-templates select="$lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin]" mode="ac:label"/> + <xsl:apply-templates select="$lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin]" mode="ac:label"/> </xsl:value-of> <xsl:text> - </xsl:text> </xsl:if> @@ -331,8 +329,8 @@ LIMIT 100 </xsl:for-each> </xsl:for-each> - <xsl:if test="$lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin]"> - <meta property="og:site_name" content="{ac:label($lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin])}"/> + <xsl:if test="$lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin]"> + <meta property="og:site_name" content="{ac:label($lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin])}"/> </xsl:if> </xsl:template> @@ -555,12 +553,12 @@ LIMIT 100 <xsl:template match="rdf:RDF | srx:sparql" mode="bs2:Brand"> <a class="brand" href="{$ldt:base}"> - <xsl:if test="$lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin]/rdf:type/@rdf:resource = '&lapp;AdminApplication'"> + <xsl:if test="$lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin]/rdf:type/@rdf:resource = '&lapp;AdminApplication'"> <xsl:attribute name="class" select="'brand admin'"/> </xsl:if> <xsl:value-of> - <xsl:apply-templates select="$lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin]" mode="ac:label"/> + <xsl:apply-templates select="$lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin]" mode="ac:label"/> </xsl:value-of> </a> </xsl:template> @@ -719,14 +717,14 @@ LIMIT 100 </button> <ul class="dropdown-menu pull-right"> <xsl:variable name="apps" select="document($app-request-uri)" as="document-node()"/> - <xsl:for-each select="$apps//*[ldh:origin/@rdf:resource]"> + <xsl:for-each select="$apps//*[lapp:origin/@rdf:resource]"> <xsl:sort select="ac:label(.)" order="ascending" lang="{$ldt:lang}"/> <li> <!-- <xsl:if test="$active"> <xsl:attribute name="class" select="'active'"/> </xsl:if>--> - <a href="{ldh:origin/@rdf:resource}" title="{ldh:origin/@rdf:resource}"> + <a href="{lapp:origin/@rdf:resource}" title="{lapp:origin/@rdf:resource}"> <xsl:apply-templates select="." mode="ac:label"/> </a> </li> @@ -768,7 +766,7 @@ LIMIT 100 <xsl:param name="google-signup" select="exists($google:clientID)" as="xs:boolean"/> <xsl:param name="orcid-signup" select="exists($orcid:clientID)" as="xs:boolean"/> <xsl:param name="webid-signup" select="$ldhc:enableWebIDSignUp" as="xs:boolean"/> - <xsl:param name="webid-signup-uri" select="resolve-uri('sign%20up', $lapp:Application//*[rdf:type/@rdf:resource = '&lapp;AdminApplication']/ldh:origin/@rdf:resource)" as="xs:anyURI"/> + <xsl:param name="webid-signup-uri" select="resolve-uri('sign%20up', $lapp:Application//*[rdf:type/@rdf:resource = '&lapp;AdminApplication']/lapp:origin/@rdf:resource)" as="xs:anyURI"/> <!-- OAuth providers dropdown --> <xsl:if test="$google-signup or $orcid-signup"> @@ -807,7 +805,7 @@ LIMIT 100 <!-- WebID signup - separate button --> <xsl:if test="$webid-signup"> <div class="pull-right"> - <a class="btn btn-primary" href="{if (not(starts-with($ldt:base, $ldh:origin))) then ac:build-uri((), map{ 'uri': string($webid-signup-uri) }) else $webid-signup-uri}"> + <a class="btn btn-primary" href="{if (not(starts-with($ldt:base, $lapp:origin))) then ac:build-uri((), map{ 'uri': string($webid-signup-uri) }) else $webid-signup-uri}"> <xsl:value-of> <xsl:apply-templates select="key('resources', 'sign-up', document('translations.rdf'))" mode="ac:label"/> </xsl:value-of> @@ -818,7 +816,7 @@ LIMIT 100 <xsl:template match="*" mode="bs2:SignUp"/> - <xsl:template match="*[ldh:origin/@rdf:resource]" mode="bs2:AppListItem"> + <xsl:template match="*[lapp:origin/@rdf:resource]" mode="bs2:AppListItem"> <xsl:param name="active" as="xs:boolean?"/> <li> @@ -826,7 +824,7 @@ LIMIT 100 <xsl:attribute name="class" select="'active'"/> </xsl:if> - <a href="{ldh:origin/@rdf:resource}" title="{ldh:origin/@rdf:resource}"> + <a href="{lapp:origin/@rdf:resource}" title="{lapp:origin/@rdf:resource}"> <xsl:value-of> <xsl:apply-templates select="." mode="ac:label"/> </xsl:value-of> @@ -1249,10 +1247,10 @@ LIMIT 100 </button> <ul class="dropdown-menu"> - <xsl:if test="$foaf:Agent//@rdf:about and $lapp:Application//*[ldh:origin/@rdf:resource = $ldh:origin]/rdf:type/@rdf:resource = '&lapp;EndUserApplication'"> + <xsl:if test="$foaf:Agent//@rdf:about and $lapp:Application//*[lapp:origin/@rdf:resource = $lapp:origin]/rdf:type/@rdf:resource = '&lapp;EndUserApplication'"> <li> <xsl:for-each select="$lapp:Application"> - <a href="{key('resources', //*[ldh:origin/@rdf:resource = $ldh:origin]/lapp:adminApplication/(@rdf:resource, @rdf:nodeID))/ldh:origin/@rdf:resource}" target="_blank"> + <a href="{key('resources', //*[lapp:origin/@rdf:resource = $lapp:origin]/lapp:adminApplication/(@rdf:resource, @rdf:nodeID))/lapp:origin/@rdf:resource}" target="_blank"> <xsl:value-of> <xsl:apply-templates select="key('resources', 'administration', document('translations.rdf'))" mode="ac:label"/> </xsl:value-of> 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 e3f35cd31..ef4d1ff92 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -510,11 +510,11 @@ WHERE <xsl:result-document href="#breadcrumb-nav" method="ixsl:replace-content"> <!-- show label if the resource is external --> <xsl:if test="not(starts-with($uri, $ldt:base))"> - <xsl:variable name="app" select="ixsl:get(ixsl:window(), 'LinkedDataHub.apps')//rdf:Description[ldh:origin/@rdf:resource = ldh:origin(ldt:base())]" as="element()?"/> + <xsl:variable name="app" select="ixsl:get(ixsl:window(), 'LinkedDataHub.apps')//rdf:Description[lapp:origin/@rdf:resource = lapp:origin(ldt:base())]" as="element()?"/> <xsl:choose> <!-- if a known app matches $uri, show link to its ldt:base --> <xsl:when test="$app"> - <a href="{$app/ldh:origin/@rdf:resource}" class="label label-info pull-left"> + <a href="{$app/lapp:origin/@rdf:resource}" class="label label-info pull-left"> <xsl:apply-templates select="$app" mode="ac:label"/> </a> </xsl:when> @@ -750,7 +750,7 @@ WHERE <xsl:when test="starts-with(?media-type, 'application/xhtml+xml')"> <xsl:variable name="endpoint-link" select="tokenize(?headers?link, ',')[contains(., '&sd;endpoint')]" as="xs:string?"/> <xsl:variable name="endpoint" select="if ($endpoint-link) then xs:anyURI(substring-before(substring-after(substring-before($endpoint-link, ';'), '<'), '>')) else ()" as="xs:anyURI?"/> - <xsl:variable name="base" select="ldh:origin($href)" as="xs:anyURI"/> + <xsl:variable name="base" select="lapp:origin($href)" as="xs:anyURI"/> <!-- set new base URI if the current app has changed --> <xsl:if test="not($base = ldt:base())"> <xsl:message>Application change. Base URI: <xsl:value-of select="$base"/></xsl:message> From b385d45b3fc6307783003c76bef82aac72ee366d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Mon, 22 Dec 2025 14:53:32 +0200 Subject: [PATCH 06/12] Replaced document-level layout mode tabs with a mode dropdown --- .../com/atomgraph/linkeddatahub/ldh.ttl | 4 ++ .../atomgraph/linkeddatahub/css/bootstrap.css | 4 ++ .../xsl/bootstrap/2.3.2/admin/signup.xsl | 4 +- .../xsl/bootstrap/2.3.2/client/form.xsl | 2 +- .../xsl/bootstrap/2.3.2/client/functions.xsl | 8 ++- .../xsl/bootstrap/2.3.2/document.xsl | 67 ++++++++++++++----- .../xsl/bootstrap/2.3.2/layout.xsl | 29 ++++---- .../xsl/bootstrap/2.3.2/resource.xsl | 27 ++++++-- .../atomgraph/linkeddatahub/xsl/client.xsl | 2 +- 9 files changed, 100 insertions(+), 47 deletions(-) diff --git a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl index 60227e530..00fd5770b 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl @@ -522,6 +522,10 @@ ac:Timeline a rdfs:Class, owl:Class ; # individuals +:ContentMode a ac:Mode ; + rdfs:label "Content" ; + rdfs:isDefinedBy : . + ac:ChartMode a ac:Mode, ac:ContainerMode ; rdfs:label "Chart" ; rdfs:isDefinedBy ac: . diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css index 24ee6b806..9977dcbbe 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css @@ -64,9 +64,13 @@ li button.btn-edit-constructors, li button.btn-add-data, li button.btn-add-ontol .btn.dropdown-toggle.btn-map { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_map_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } .btn.dropdown-toggle.btn-graph { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_blur_on_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } .btn.dropdown-toggle.btn-query { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_code_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } +.btn.dropdown-toggle.btn-content { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/view_list_black_24dp.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } +.btn.dropdown-toggle.btn-chart { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_show_chart_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } +.dropdown-menu > li > a.btn-content { background-image: url('../icons/view_list_black_24dp.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-read { background-image: url('../icons/ic_details_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-edit { background-image: url('../icons/ic_create_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-map { background-image: url('../icons/ic_map_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } +.dropdown-menu > li > a.btn-chart { background-image: url('../icons/ic_show_chart_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-graph { background-image: url('../icons/ic_blur_on_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-query { background-image: url('../icons/ic_code_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } #doc-tree { display: none; width: 15%; position: fixed; left: 0; top: 106px; height: calc(100% - 106px); } diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/signup.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/signup.xsl index 6e56358fb..196a41081 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/signup.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/signup.xsl @@ -59,8 +59,8 @@ exclude-result-prefixes="#all"> <!-- hide "Add data" button which otherwise would be shown because acl:Append is allowed for signup --> <xsl:template match="rdf:RDF[ac:absolute-path(ldh:request-uri()) = resolve-uri(encode-for-uri('sign up'), $ldt:base)]" mode="bs2:AddData" priority="2" use-when="system-property('xsl:product-name') = 'SAXON'"/> - - <xsl:template match="rdf:RDF[ac:absolute-path(ldh:request-uri()) = resolve-uri(encode-for-uri('sign up'), $ldt:base)]" mode="bs2:ModeTabs" priority="2" use-when="system-property('xsl:product-name') = 'SAXON'"/> + + <xsl:template match="rdf:RDF[ac:absolute-path(ldh:request-uri()) = resolve-uri(encode-for-uri('sign up'), $ldt:base)]" mode="bs2:ModeList" priority="2" use-when="system-property('xsl:product-name') = 'SAXON'"/> <!-- disable the right nav (backlinks etc.) --> <xsl:template match="*[*][@rdf:about or @rdf:nodeID][ac:absolute-path(ldh:request-uri()) = resolve-uri(encode-for-uri('sign up'), $ldt:base)]" mode="bs2:Right" use-when="system-property('xsl:product-name') = 'SAXON'"/> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl index b04c4fa8f..9e71186f8 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl @@ -298,7 +298,7 @@ WHERE <!-- open a form for document editing --> - <xsl:template match="div[contains-token(@class, 'navbar')]//div[@id = 'doc-controls']//button[contains-token(@class, 'btn-edit')]" mode="ixsl:onclick"> + <xsl:template match="div[contains-token(@class, 'action-bar')]//button[contains-token(@class, 'btn-edit')]" mode="ixsl:onclick"> <xsl:param name="about" select="ac:absolute-path(ldh:base-uri(.))" as="xs:anyURI"/> <!-- editing the current document resources --> <xsl:param name="method" select="'patch'" as="xs:string"/> <xsl:param name="form-actions-class" select="'form-actions modal-footer'" as="xs:string?"/> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl index a411b137b..5731d47c9 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl @@ -76,7 +76,9 @@ exclude-result-prefixes="#all" </xsl:function> <xsl:function name="ac:mode" as="xs:anyURI*"> - <xsl:variable name="nav-tab-class" select="id('layout-modes', ixsl:page())/li[contains-token(@class, 'active')]/@class" as="xs:string"/> + <xsl:variable name="mode-button" select="id('layout-modes', ixsl:page())" as="element()?"/> + <xsl:variable name="dropdown-menu" select="$mode-button/following-sibling::ul[contains-token(@class, 'dropdown-menu')]" as="element()?"/> + <xsl:variable name="active-item-class" select="$dropdown-menu/li[contains-token(@class, 'active')]/@class" as="xs:string?"/> <xsl:variable name="mode-classes" as="map(xs:string, xs:string)"> <xsl:map> <xsl:map-entry key="'content-mode'" select="'&ldh;ContentMode'"/> @@ -86,8 +88,8 @@ exclude-result-prefixes="#all" <xsl:map-entry key="'graph-mode'" select="'∾GraphMode'"/> </xsl:map> </xsl:variable> - <xsl:variable name="mode-class" select="map:keys($mode-classes)[contains-token($nav-tab-class, .)]" as="xs:string"/> - <xsl:sequence select="xs:anyURI(map:get($mode-classes, $mode-class))"/> + <xsl:variable name="mode-class" select="map:keys($mode-classes)[contains-token($active-item-class, .)]" as="xs:string?"/> + <xsl:sequence select="if ($mode-class) then xs:anyURI(map:get($mode-classes, $mode-class)) else ()"/> </xsl:function> <xsl:function name="sd:endpoint" as="xs:anyURI"> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl index 6dd88aa65..0c4a1a9d9 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl @@ -132,29 +132,62 @@ extension-element-prefixes="ixsl" </div> </xsl:template> - <!-- MODE TABS --> - - <xsl:template match="rdf:RDF" mode="bs2:ModeTabs" use-when="system-property('xsl:product-name') = 'SAXON'"> + <!-- MODE LIST --> + + <xsl:template match="rdf:RDF" mode="bs2:ModeList" priority="2" use-when="system-property('xsl:product-name') = 'SAXON'"> <xsl:param name="has-content" as="xs:boolean"/> <xsl:param name="active-mode" as="xs:anyURI?"/> <xsl:param name="ajax-rendering" select="true()" as="xs:boolean"/> - <xsl:param name="absolute-path" select="ac:absolute-path(ldh:request-uri())" as="xs:anyURI"/> <!-- make sure mode tabs always link to the local document (not the proxy-loaded doc) --> + <xsl:param name="absolute-path" select="ac:absolute-path(ldh:request-uri())" as="xs:anyURI"/> <xsl:param name="base-uri" select="ldh:base-uri(.)" as="xs:anyURI"/> <xsl:param name="id" select="'layout-modes'" as="xs:string?"/> - <xsl:param name="class" select="'nav nav-tabs offset2 span7'" as="xs:string?"/> - <div class="row-fluid"> - <ul> + <div class="btn-group pull-right"> + <button type="button" title="{ac:label(key('resources', '∾Mode', document(ac:document-uri('∾'))))}"> <xsl:if test="$id"> <xsl:attribute name="id" select="$id"/> </xsl:if> - <xsl:if test="$class"> - <xsl:attribute name="class" select="$class"/> - </xsl:if> - + + <xsl:choose> + <xsl:when test="$active-mode = '&ldh;ContentMode'"> + <xsl:apply-templates select="key('resources', '&ldh;ContentMode', document(ac:document-uri('&ldh;')))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> + </xsl:when> + <xsl:when test="$active-mode = '∾ReadMode'"> + <xsl:apply-templates select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> + </xsl:when> + <xsl:when test="$active-mode = '∾MapMode'"> + <xsl:apply-templates select="key('resources', '∾MapMode', document(ac:document-uri('∾')))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> + </xsl:when> + <xsl:when test="$active-mode = '∾ChartMode'"> + <xsl:apply-templates select="key('resources', '∾ChartMode', document(ac:document-uri('∾')))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> + </xsl:when> + <xsl:when test="$active-mode = '∾GraphMode'"> + <xsl:apply-templates select="key('resources', '∾GraphMode', document(ac:document-uri('∾')))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> + </xsl:otherwise> + </xsl:choose> + <xsl:text> </xsl:text> + <span class="caret"></span> + </button> + + <ul class="dropdown-menu"> <li class="content-mode{if ((empty($active-mode) and $has-content) or $active-mode = '&ldh;ContentMode') then ' active' else() }"> - <!-- make sure mode tabs always link to the local document (not the proxy-loaded doc) --> <a href="{ldh:href(ac:document-uri(ldh:base-uri(.)), ldh:query-params(xs:anyURI('&ldh;ContentMode')))}"> + <xsl:apply-templates select="key('resources', '&ldh;ContentMode', document(ac:document-uri('&ldh;')))" mode="ldh:logo"/> <xsl:value-of> <xsl:apply-templates select="key('resources', 'content', document('translations.rdf'))" mode="ac:label"/> </xsl:value-of> @@ -162,14 +195,14 @@ extension-element-prefixes="ixsl" </li> <xsl:for-each select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeTabsItem"> + <xsl:apply-templates select="." mode="bs2:ModeListItem"> <xsl:with-param name="active" select="@rdf:about = $active-mode or (empty($active-mode) and not($has-content))"/> <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> <xsl:with-param name="base-uri" select="$base-uri"/> </xsl:apply-templates> </xsl:for-each> <xsl:for-each select="key('resources', '∾MapMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeTabsItem"> + <xsl:apply-templates select="." mode="bs2:ModeListItem"> <xsl:with-param name="active" select="@rdf:about = $active-mode"/> <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> <xsl:with-param name="base-uri" select="$base-uri"/> @@ -177,7 +210,7 @@ extension-element-prefixes="ixsl" </xsl:for-each> <xsl:if test="$ajax-rendering"> <xsl:for-each select="key('resources', '∾ChartMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeTabsItem"> + <xsl:apply-templates select="." mode="bs2:ModeListItem"> <xsl:with-param name="active" select="@rdf:about = $active-mode"/> <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> <xsl:with-param name="base-uri" select="$base-uri"/> @@ -185,7 +218,7 @@ extension-element-prefixes="ixsl" </xsl:for-each> </xsl:if> <xsl:for-each select="key('resources', '∾GraphMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeTabsItem"> + <xsl:apply-templates select="." mode="bs2:ModeListItem"> <xsl:with-param name="active" select="@rdf:about = $active-mode"/> <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> <xsl:with-param name="base-uri" select="$base-uri"/> @@ -194,7 +227,7 @@ extension-element-prefixes="ixsl" </ul> </div> </xsl:template> - + <!-- CONTENT LIST --> <xsl:template match="rdf:RDF" mode="ldh:ContentList"> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl index 8b8f21876..14cdf1a60 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl @@ -647,12 +647,6 @@ LIMIT 100 <div id="doc-controls" class="span4"> <xsl:apply-templates select="key('resources', ac:absolute-path(ldh:base-uri(.)))" mode="bs2:Timestamp"/> - - <xsl:if test="$acl:mode = '&acl;Write'"> - <button type="button" class="btn btn-edit pull-right"> - <xsl:apply-templates select="key('resources', '∾EditMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </button> - </xsl:if> </div> </div> </div> @@ -669,12 +663,18 @@ LIMIT 100 <xsl:if test="$class"> <xsl:attribute name="class" select="$class"/> </xsl:if> - + <xsl:apply-templates select="." mode="bs2:MediaTypeList"> <xsl:with-param name="uri" select="ac:absolute-path(ldh:base-uri(.))"/> </xsl:apply-templates> <xsl:apply-templates select="." mode="bs2:NavBarActions"/> + + <xsl:apply-templates select="." mode="bs2:ModeList"> + <xsl:with-param name="has-content" select="$has-content"/> + <xsl:with-param name="active-mode" select="$ac:mode"/> + <xsl:with-param name="ajax-rendering" select="$ldh:ajaxRendering"/> + </xsl:apply-templates> </div> </xsl:template> @@ -867,12 +867,6 @@ LIMIT 100 <xsl:if test="exists($typeof)"> <xsl:attribute name="typeof" select="string-join($typeof, ' ')"/> </xsl:if> - - <xsl:apply-templates select="." mode="bs2:ModeTabs"> - <xsl:with-param name="has-content" select="$has-content"/> - <xsl:with-param name="active-mode" select="$ac:mode"/> - <xsl:with-param name="ajax-rendering" select="$ldh:ajaxRendering"/> - </xsl:apply-templates> <xsl:choose> <!-- error responses always rendered in bs2:Row mode, no matter what $ac:mode specifies --> @@ -917,9 +911,6 @@ LIMIT 100 </xsl:choose> </div> </xsl:template> - - <!-- don't show document-level tabs if the response returned an error or if we're in EditMode --> - <xsl:template match="rdf:RDF[key('resources-by-type', '&http;Response')]" mode="bs2:ModeTabs" priority="1"/> <xsl:template match="srx:sparql" mode="bs2:ContentBody"> <xsl:param name="id" select="'content-body'" as="xs:string?"/> @@ -1211,6 +1202,12 @@ LIMIT 100 <xsl:apply-templates select="key('resources', '&acl;Access', document(ac:document-uri('&acl;')))" mode="ac:label"/> </button> </div> + + <xsl:if test="$acl:mode = '&acl;Write'"> + <button type="button" class="btn btn-edit pull-right"> + <xsl:apply-templates select="key('resources', '∾EditMode', document(ac:document-uri('∾')))" mode="ac:label"/> + </button> + </xsl:if> </xsl:if> </xsl:template> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl index a4780e334..354241ca9 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl @@ -261,21 +261,33 @@ extension-element-prefixes="ixsl" <xsl:attribute name="class" select="concat($class, ' ', 'btn-agent')"/> </xsl:template> + <xsl:template match="*[@rdf:about = '&ldh;ContentMode']" mode="ldh:logo"> + <xsl:param name="class" as="xs:string?"/> + + <xsl:attribute name="class" select="concat($class, ' ', 'btn-content')"/> + </xsl:template> + <xsl:template match="*[@rdf:about = '∾ReadMode']" mode="ldh:logo"> <xsl:param name="class" as="xs:string?"/> - + <xsl:attribute name="class" select="concat($class, ' ', 'btn-read')"/> </xsl:template> <xsl:template match="*[@rdf:about = '∾MapMode']" mode="ldh:logo"> <xsl:param name="class" as="xs:string?"/> - + <xsl:attribute name="class" select="concat($class, ' ', 'btn-map')"/> </xsl:template> + <xsl:template match="*[@rdf:about = '∾ChartMode']" mode="ldh:logo"> + <xsl:param name="class" as="xs:string?"/> + + <xsl:attribute name="class" select="concat($class, ' ', 'btn-chart')"/> + </xsl:template> + <xsl:template match="*[@rdf:about = '∾GraphMode']" mode="ldh:logo"> <xsl:param name="class" as="xs:string?"/> - + <xsl:attribute name="class" select="concat($class, ' ', 'btn-graph')"/> </xsl:template> @@ -397,9 +409,9 @@ extension-element-prefixes="ixsl" </div> </xsl:template> - <!-- MODE TABS --> - - <xsl:template match="*[@rdf:about]" mode="bs2:ModeTabsItem"> + <!-- MODE LIST --> + + <xsl:template match="*[@rdf:about]" mode="bs2:ModeListItem"> <xsl:param name="absolute-path" select="ac:absolute-path(ldh:base-uri(.))" as="xs:anyURI" tunnel="yes"/> <xsl:param name="base-uri" as="xs:anyURI?"/> <xsl:param name="active" as="xs:boolean"/> @@ -420,13 +432,14 @@ extension-element-prefixes="ixsl" </xsl:if> <a href="{ldh:href(ac:document-uri($base-uri), ldh:query-params(xs:anyURI(@rdf:about)))}"> + <xsl:apply-templates select="." mode="ldh:logo"/> <xsl:value-of> <xsl:apply-templates select="." mode="ac:label"/> </xsl:value-of> </a> </li> </xsl:template> - + <!-- DEFAULT --> <!-- embed file content --> 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 ef4d1ff92..44f3a7599 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -835,7 +835,7 @@ WHERE <xsl:if test="$push-state"> <xsl:call-template name="ldh:PushState"> - <xsl:with-param name="href" select="ldh:href($href, map{})"/> + <xsl:with-param name="href" select="ldh:href($href, ldh:query-params(ac:mode()))"/> <xsl:with-param name="title" select="/html/head/title"/> <xsl:with-param name="container" select="$container"/> </xsl:call-template> From 816bfd7dcb7f6cd998935bed7ecc8c79b0f05ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Tue, 23 Dec 2025 18:10:01 +0100 Subject: [PATCH 07/12] View mode tabs replaced with a dropdown Improved mode rendering templates --- .../atomgraph/linkeddatahub/css/bootstrap.css | 6 + .../xsl/bootstrap/2.3.2/client/block/view.xsl | 131 +++++++----------- .../xsl/bootstrap/2.3.2/document.xsl | 64 ++------- .../xsl/bootstrap/2.3.2/resource.xsl | 49 +++---- 4 files changed, 83 insertions(+), 167 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css index 9977dcbbe..4a8f2e490 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css @@ -66,6 +66,9 @@ li button.btn-edit-constructors, li button.btn-add-data, li button.btn-add-ontol .btn.dropdown-toggle.btn-query { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_code_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } .btn.dropdown-toggle.btn-content { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/view_list_black_24dp.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } .btn.dropdown-toggle.btn-chart { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_show_chart_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } +.btn.dropdown-toggle.btn-list { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/view_list_black_24dp.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } +.btn.dropdown-toggle.btn-table { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_border_all_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } +.btn.dropdown-toggle.btn-grid { text-align: right; font-size: 0; color: transparent; background-image: url('../icons/ic_grid_on_black_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 64px; } .dropdown-menu > li > a.btn-content { background-image: url('../icons/view_list_black_24dp.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-read { background-image: url('../icons/ic_details_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-edit { background-image: url('../icons/ic_create_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } @@ -73,6 +76,9 @@ li button.btn-edit-constructors, li button.btn-add-data, li button.btn-add-ontol .dropdown-menu > li > a.btn-chart { background-image: url('../icons/ic_show_chart_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-graph { background-image: url('../icons/ic_blur_on_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } .dropdown-menu > li > a.btn-query { background-image: url('../icons/ic_code_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } +.dropdown-menu > li > a.btn-list { background-image: url('../icons/view_list_black_24dp.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } +.dropdown-menu > li > a.btn-table { background-image: url('../icons/ic_border_all_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } +.dropdown-menu > li > a.btn-grid { background-image: url('../icons/ic_grid_on_black_24px.svg'); background-position: 12px center; background-repeat: no-repeat; padding: 5px 5px 5px 40px; } #doc-tree { display: none; width: 15%; position: fixed; left: 0; top: 106px; height: calc(100% - 106px); } @media (max-width: 979px) { diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl index 8db2c10f2..129bae35c 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl @@ -441,83 +441,46 @@ exclude-result-prefixes="#all" </xsl:if> </xsl:template> - <!-- view mode tabs --> - - <xsl:template name="bs2:ViewModeTabs"> - <xsl:param name="active-mode" as="xs:anyURI"/> - - <ul class="nav nav-tabs view-mode-nav-tabs"> - <li class="read-mode"> - <xsl:if test="$active-mode = '∾ReadMode'"> - <xsl:attribute name="class" select="'read-mode active'"/> - </xsl:if> + <!-- view mode dropdown --> - <a> - <xsl:apply-templates select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - <li class="list-mode"> - <xsl:if test="$active-mode = '∾ListMode'"> - <xsl:attribute name="class" select="'list-mode active'"/> - </xsl:if> - - <a> - <xsl:apply-templates select="key('resources', '∾ListMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾ListMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - <li class="table-mode"> - <xsl:if test="$active-mode = '∾TableMode'"> - <xsl:attribute name="class" select="'table-mode active'"/> - </xsl:if> + <xsl:template name="bs2:ViewModeList"> + <xsl:param name="active-mode" as="xs:anyURI"/> + <xsl:param name="id" select="'view-modes'" as="xs:string?"/> + <xsl:param name="mode-button-classes" as="map(xs:string, xs:string)"> + <xsl:map> + <xsl:map-entry key="'∾ReadMode'" select="'btn-read'"/> + <xsl:map-entry key="'∾ListMode'" select="'btn-list'"/> + <xsl:map-entry key="'∾TableMode'" select="'btn-table'"/> + <xsl:map-entry key="'∾GridMode'" select="'btn-grid'"/> + <xsl:map-entry key="'∾ChartMode'" select="'btn-chart'"/> + <xsl:map-entry key="'∾MapMode'" select="'btn-map'"/> + <xsl:map-entry key="'∾GraphMode'" select="'btn-graph'"/> + </xsl:map> + </xsl:param> - <a> - <xsl:apply-templates select="key('resources', '∾TableMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾TableMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - <li class="grid-mode"> - <xsl:if test="$active-mode = '∾GridMode'"> - <xsl:attribute name="class" select="'grid-mode active'"/> + <div class="btn-group pull-right"> + <button type="button" title="{ac:label(key('resources', '∾Mode', document(ac:document-uri('∾'))))}"> + <xsl:if test="$id"> + <xsl:attribute name="id" select="$id"/> </xsl:if> - <a> - <xsl:apply-templates select="key('resources', '∾GridMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾GridMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - <li class="chart-mode"> - <xsl:if test="$active-mode = '∾ChartMode'"> - <xsl:attribute name="class" select="'chart-mode active'"/> - </xsl:if> + <xsl:attribute name="class" select="'btn dropdown-toggle ' || (map:get($mode-button-classes, string($active-mode)), 'btn-read')[1]"/> - <a> - <xsl:apply-templates select="key('resources', '∾ChartMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾ChartMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - <li class="map-mode"> - <xsl:if test="$active-mode = '∾MapMode'"> - <xsl:attribute name="class" select="'map-mode active'"/> - </xsl:if> + <xsl:text> </xsl:text> + <span class="caret"></span> + </button> - <a> - <xsl:apply-templates select="key('resources', '∾MapMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾MapMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - <li class="graph-mode"> - <xsl:if test="$active-mode = '∾GraphMode'"> - <xsl:attribute name="class" select="'graph-mode active'"/> - </xsl:if> - - <a> - <xsl:apply-templates select="key('resources', '∾GraphMode', document(ac:document-uri('∾')))" mode="ldh:logo"/> - <xsl:apply-templates select="key('resources', '∾GraphMode', document(ac:document-uri('∾')))" mode="ac:label"/> - </a> - </li> - </ul> + <ul class="dropdown-menu view-mode-list"> + <xsl:for-each select="('∾ReadMode', '∾ListMode', '∾TableMode', '∾GridMode', '∾ChartMode', '∾MapMode', '∾GraphMode')"> + <xsl:for-each select="key('resources', ., document(ac:document-uri('∾')))"> + <xsl:apply-templates select="." mode="bs2:ModeListItem"> + <xsl:with-param name="active" select="@rdf:about = $active-mode"/> + <xsl:with-param name="href" select="()"/> + </xsl:apply-templates> + </xsl:for-each> + </xsl:for-each> + </ul> + </div> </xsl:template> <!-- render view --> @@ -748,6 +711,10 @@ exclude-result-prefixes="#all" <xsl:value-of select="$container/descendant::*[@property = '&dct;title']"/> </h2> + <xsl:call-template name="bs2:ViewModeList"> + <xsl:with-param name="active-mode" select="$active-mode"/> + </xsl:call-template> + <div class="pull-right"> <form class="form-inline"> <label for="{$order-by-container-id}"> @@ -790,10 +757,6 @@ exclude-result-prefixes="#all" <div> <p id="{$result-count-container-id}" class="result-count"/> - <xsl:call-template name="bs2:ViewModeTabs"> - <xsl:with-param name="active-mode" select="$active-mode"/> - </xsl:call-template> - <div id="{$container-results-id}" class="container-results"></div> </div> </xsl:result-document> @@ -1146,9 +1109,9 @@ exclude-result-prefixes="#all" <!-- EVENT LISTENERS --> - <!-- view mode tabs --> - - <xsl:template match="*[@typeof]//div/ul[contains-token(@class, 'view-mode-nav-tabs')]/li[not(contains-token(@class, 'active'))]/a" mode="ixsl:onclick"> + <!-- view mode dropdown --> + + <xsl:template match="*[@typeof]//ul[contains-token(@class, 'view-mode-list')]/li[not(contains-token(@class, 'active'))]/a" mode="ixsl:onclick"> <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> @@ -1203,7 +1166,7 @@ exclude-result-prefixes="#all" <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> - <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-nav-tabs')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> + <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-list')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> <xsl:variable name="active-mode" select="map:get($class-modes, $active-class)" as="xs:anyURI"/> <xsl:variable name="select-string" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-query')" as="xs:string"/> <xsl:variable name="select-xml" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-xml')" as="document-node()"/> @@ -1253,7 +1216,7 @@ exclude-result-prefixes="#all" <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> - <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-nav-tabs')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> + <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-list')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> <xsl:variable name="active-mode" select="map:get($class-modes, $active-class)" as="xs:anyURI"/> <xsl:variable name="select-string" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-query')" as="xs:string"/> <xsl:variable name="select-xml" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-xml')" as="document-node()"/> @@ -1302,7 +1265,7 @@ exclude-result-prefixes="#all" <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> - <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-nav-tabs')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> + <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-list')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> <xsl:variable name="active-mode" select="map:get($class-modes, $active-class)" as="xs:anyURI"/> <xsl:variable name="predicate" select="ixsl:get(., 'value')" as="xs:anyURI?"/> <xsl:variable name="select-string" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-query')" as="xs:string"/> @@ -1352,7 +1315,7 @@ exclude-result-prefixes="#all" <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> - <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-nav-tabs')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> + <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-list')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> <xsl:variable name="active-mode" select="map:get($class-modes, $active-class)" as="xs:anyURI"/> <xsl:variable name="desc" select="contains(@class, 'btn-order-by-desc')" as="xs:boolean"/> <xsl:variable name="select-string" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-query')" as="xs:string"/> @@ -1498,7 +1461,7 @@ exclude-result-prefixes="#all" <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> - <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-nav-tabs')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> + <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-list')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> <xsl:variable name="active-mode" select="map:get($class-modes, $active-class)" as="xs:anyURI"/> <xsl:variable name="var-name" select="@name" as="xs:string"/> <!-- collect the values/types/datatypes of all checked inputs within this facet and build an array of maps --> @@ -1549,7 +1512,7 @@ exclude-result-prefixes="#all" <xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/> <xsl:variable name="container" select="ancestor::div[@typeof][1]" as="element()"/> <xsl:variable name="block-uri" select="xs:anyURI($block/@about)" as="xs:anyURI"/> - <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-nav-tabs')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> + <xsl:variable name="active-class" select="tokenize($container//ul[contains-token(@class, 'view-mode-list')]/li[contains-token(@class, 'active')]/@class, ' ')[not(. = 'active')]" as="xs:string"/> <xsl:variable name="active-mode" select="map:get($class-modes, $active-class)" as="xs:anyURI"/> <xsl:variable name="predicate" select="input/@value" as="xs:anyURI"/> <xsl:variable name="select-string" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block-uri || '`'), 'select-query')" as="xs:string"/> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl index 0c4a1a9d9..ebd5c61ec 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl @@ -148,38 +148,10 @@ extension-element-prefixes="ixsl" <xsl:attribute name="id" select="$id"/> </xsl:if> - <xsl:choose> - <xsl:when test="$active-mode = '&ldh;ContentMode'"> - <xsl:apply-templates select="key('resources', '&ldh;ContentMode', document(ac:document-uri('&ldh;')))" mode="ldh:logo"> - <xsl:with-param name="class" select="'btn dropdown-toggle'"/> - </xsl:apply-templates> - </xsl:when> - <xsl:when test="$active-mode = '∾ReadMode'"> - <xsl:apply-templates select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))" mode="ldh:logo"> - <xsl:with-param name="class" select="'btn dropdown-toggle'"/> - </xsl:apply-templates> - </xsl:when> - <xsl:when test="$active-mode = '∾MapMode'"> - <xsl:apply-templates select="key('resources', '∾MapMode', document(ac:document-uri('∾')))" mode="ldh:logo"> - <xsl:with-param name="class" select="'btn dropdown-toggle'"/> - </xsl:apply-templates> - </xsl:when> - <xsl:when test="$active-mode = '∾ChartMode'"> - <xsl:apply-templates select="key('resources', '∾ChartMode', document(ac:document-uri('∾')))" mode="ldh:logo"> - <xsl:with-param name="class" select="'btn dropdown-toggle'"/> - </xsl:apply-templates> - </xsl:when> - <xsl:when test="$active-mode = '∾GraphMode'"> - <xsl:apply-templates select="key('resources', '∾GraphMode', document(ac:document-uri('∾')))" mode="ldh:logo"> - <xsl:with-param name="class" select="'btn dropdown-toggle'"/> - </xsl:apply-templates> - </xsl:when> - <xsl:otherwise> - <xsl:apply-templates select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))" mode="ldh:logo"> - <xsl:with-param name="class" select="'btn dropdown-toggle'"/> - </xsl:apply-templates> - </xsl:otherwise> - </xsl:choose> + <xsl:variable name="effective-mode" select="if ($active-mode) then $active-mode else '∾ReadMode'" as="xs:anyURI"/> + <xsl:apply-templates select="key('resources', $effective-mode, document(ac:document-uri(string($effective-mode))))" mode="ldh:logo"> + <xsl:with-param name="class" select="'btn dropdown-toggle'"/> + </xsl:apply-templates> <xsl:text> </xsl:text> <span class="caret"></span> </button> @@ -194,35 +166,15 @@ extension-element-prefixes="ixsl" </a> </li> - <xsl:for-each select="key('resources', '∾ReadMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeListItem"> - <xsl:with-param name="active" select="@rdf:about = $active-mode or (empty($active-mode) and not($has-content))"/> - <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> - <xsl:with-param name="base-uri" select="$base-uri"/> - </xsl:apply-templates> - </xsl:for-each> - <xsl:for-each select="key('resources', '∾MapMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeListItem"> - <xsl:with-param name="active" select="@rdf:about = $active-mode"/> - <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> - <xsl:with-param name="base-uri" select="$base-uri"/> - </xsl:apply-templates> - </xsl:for-each> - <xsl:if test="$ajax-rendering"> - <xsl:for-each select="key('resources', '∾ChartMode', document(ac:document-uri('∾')))"> + <xsl:for-each select="('∾ReadMode', '∾MapMode', if ($ajax-rendering) then '∾ChartMode' else (), '∾GraphMode')"> + <xsl:variable name="mode-uri" select="." as="xs:string"/> + <xsl:for-each select="key('resources', $mode-uri, document(ac:document-uri('∾')))"> <xsl:apply-templates select="." mode="bs2:ModeListItem"> - <xsl:with-param name="active" select="@rdf:about = $active-mode"/> + <xsl:with-param name="active" select="if (@rdf:about = '∾ReadMode') then (@rdf:about = $active-mode or (empty($active-mode) and not($has-content))) else @rdf:about = $active-mode"/> <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> <xsl:with-param name="base-uri" select="$base-uri"/> </xsl:apply-templates> </xsl:for-each> - </xsl:if> - <xsl:for-each select="key('resources', '∾GraphMode', document(ac:document-uri('∾')))"> - <xsl:apply-templates select="." mode="bs2:ModeListItem"> - <xsl:with-param name="active" select="@rdf:about = $active-mode"/> - <xsl:with-param name="absolute-path" select="$absolute-path" tunnel="yes"/> - <xsl:with-param name="base-uri" select="$base-uri"/> - </xsl:apply-templates> </xsl:for-each> </ul> </div> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl index 354241ca9..dfa0b2d41 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl @@ -261,34 +261,22 @@ extension-element-prefixes="ixsl" <xsl:attribute name="class" select="concat($class, ' ', 'btn-agent')"/> </xsl:template> - <xsl:template match="*[@rdf:about = '&ldh;ContentMode']" mode="ldh:logo"> - <xsl:param name="class" as="xs:string?"/> - - <xsl:attribute name="class" select="concat($class, ' ', 'btn-content')"/> - </xsl:template> - - <xsl:template match="*[@rdf:about = '∾ReadMode']" mode="ldh:logo"> - <xsl:param name="class" as="xs:string?"/> - - <xsl:attribute name="class" select="concat($class, ' ', 'btn-read')"/> - </xsl:template> - - <xsl:template match="*[@rdf:about = '∾MapMode']" mode="ldh:logo"> - <xsl:param name="class" as="xs:string?"/> - - <xsl:attribute name="class" select="concat($class, ' ', 'btn-map')"/> - </xsl:template> - - <xsl:template match="*[@rdf:about = '∾ChartMode']" mode="ldh:logo"> - <xsl:param name="class" as="xs:string?"/> - - <xsl:attribute name="class" select="concat($class, ' ', 'btn-chart')"/> - </xsl:template> - - <xsl:template match="*[@rdf:about = '∾GraphMode']" mode="ldh:logo"> + <xsl:template match="*[@rdf:about = ('&ldh;ContentMode', '∾ReadMode', '∾ListMode', '∾TableMode', '∾GridMode', '∾MapMode', '∾ChartMode', '∾GraphMode')]" mode="ldh:logo"> <xsl:param name="class" as="xs:string?"/> + <xsl:param name="mode-logo-classes" as="map(xs:string, xs:string)"> + <xsl:map> + <xsl:map-entry key="'&ldh;ContentMode'" select="'btn-content'"/> + <xsl:map-entry key="'∾ReadMode'" select="'btn-read'"/> + <xsl:map-entry key="'∾ListMode'" select="'btn-list'"/> + <xsl:map-entry key="'∾TableMode'" select="'btn-table'"/> + <xsl:map-entry key="'∾GridMode'" select="'btn-grid'"/> + <xsl:map-entry key="'∾MapMode'" select="'btn-map'"/> + <xsl:map-entry key="'∾ChartMode'" select="'btn-chart'"/> + <xsl:map-entry key="'∾GraphMode'" select="'btn-graph'"/> + </xsl:map> + </xsl:param> - <xsl:attribute name="class" select="concat($class, ' ', 'btn-graph')"/> + <xsl:attribute name="class" select="concat($class, ' ', map:get($mode-logo-classes, @rdf:about))"/> </xsl:template> <xsl:template match="*[@rdf:about = '∾QueryEditorMode']" mode="ldh:logo"> @@ -415,10 +403,14 @@ extension-element-prefixes="ixsl" <xsl:param name="absolute-path" select="ac:absolute-path(ldh:base-uri(.))" as="xs:anyURI" tunnel="yes"/> <xsl:param name="base-uri" as="xs:anyURI?"/> <xsl:param name="active" as="xs:boolean"/> + <xsl:param name="href" select="ldh:href(ac:document-uri($base-uri), ldh:query-params(xs:anyURI(@rdf:about)))" as="xs:anyURI?"/> <xsl:param name="mode-classes" as="map(xs:string, xs:string)"> <xsl:map> <xsl:map-entry key="'&ldh;ContentMode'" select="'content-mode'"/> <xsl:map-entry key="'∾ReadMode'" select="'read-mode'"/> + <xsl:map-entry key="'∾ListMode'" select="'list-mode'"/> + <xsl:map-entry key="'∾TableMode'" select="'table-mode'"/> + <xsl:map-entry key="'∾GridMode'" select="'grid-mode'"/> <xsl:map-entry key="'∾MapMode'" select="'map-mode'"/> <xsl:map-entry key="'∾ChartMode'" select="'chart-mode'"/> <xsl:map-entry key="'∾GraphMode'" select="'graph-mode'"/> @@ -431,7 +423,10 @@ extension-element-prefixes="ixsl" <xsl:attribute name="class" select="$class"/> </xsl:if> - <a href="{ldh:href(ac:document-uri($base-uri), ldh:query-params(xs:anyURI(@rdf:about)))}"> + <a> + <xsl:if test="$href"> + <xsl:attribute name="href" select="$href"/> + </xsl:if> <xsl:apply-templates select="." mode="ldh:logo"/> <xsl:value-of> <xsl:apply-templates select="." mode="ac:label"/> From 70a0b1232e839ae6ca35520b3fe1edc0a449a7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Wed, 24 Dec 2025 10:47:47 +0100 Subject: [PATCH 08/12] Optional view heading --- .../xsl/bootstrap/2.3.2/client/block/view.xsl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl index 129bae35c..3ebcc4766 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/view.xsl @@ -707,9 +707,11 @@ exclude-result-prefixes="#all" <!-- first time rendering the view results --> <xsl:if test="$initial-load"> <xsl:result-document href="?." method="ixsl:replace-content"> - <h2> - <xsl:value-of select="$container/descendant::*[@property = '&dct;title']"/> - </h2> + <xsl:where-populated> + <h2> + <xsl:value-of select="$container/descendant::*[@property = '&dct;title']"/> + </h2> + </xsl:where-populated> <xsl:call-template name="bs2:ViewModeList"> <xsl:with-param name="active-mode" select="$active-mode"/> From 69f5594872c0dc28ff0a6fa03f1acb9733afe945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Sun, 30 Nov 2025 15:22:27 +0100 Subject: [PATCH 09/12] Package model and initial implementation --- bin/admin/install-package.sh | 101 ++++++ platform/datasets/admin.trig | 36 +++ .../atomgraph/linkeddatahub/Application.java | 1 + .../linkeddatahub/apps/model/Application.java | 16 +- .../linkeddatahub/apps/model/Package.java | 46 +++ .../apps/model/impl/ApplicationImpl.java | 29 +- .../apps/model/impl/PackageImpl.java | 59 ++++ .../model/impl/PackageImplementation.java | 56 ++++ .../resource/admin/pkg/Install.java | 304 ++++++++++++++++++ .../resource/admin/pkg/Uninstall.java | 237 ++++++++++++++ .../server/model/impl/Dispatcher.java | 26 +- .../server/model/impl/GraphStoreImpl.java | 6 +- .../server/model/impl/PackageManager.java | 170 ++++++++++ .../server/util/XsltMasterUpdater.java | 244 ++++++++++++++ .../linkeddatahub/vocabulary/LAPP.java | 5 +- .../linkeddatahub/vocabulary/LDH.java | 4 +- .../com/atomgraph/linkeddatahub/lapp.ttl | 7 + src/main/webapp/static/localhost/layout.xsl | 12 + 18 files changed, 1345 insertions(+), 14 deletions(-) create mode 100755 bin/admin/install-package.sh create mode 100644 src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImplementation.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/server/util/XsltMasterUpdater.java create mode 100644 src/main/webapp/static/localhost/layout.xsl diff --git a/bin/admin/install-package.sh b/bin/admin/install-package.sh new file mode 100755 index 000000000..fb2ce3d9a --- /dev/null +++ b/bin/admin/install-package.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +print_usage() +{ + printf "Installs a LinkedDataHub package.\n" + printf "\n" + printf "Usage: %s options\n" "$0" + printf "\n" + printf "Options:\n" + printf " -b, --base BASE_URL Base URL of the application\n" + printf " -f, --cert-pem-file CERT_FILE .pem file with the WebID certificate of the agent\n" + printf " -p, --cert-password CERT_PASSWORD Password of the WebID certificate\n" + printf " --proxy PROXY_URL The host this request will be proxied through (optional)\n" + printf " --package PACKAGE_URI URI of the package to install (e.g., https://packages.linkeddatahub.com/skos/#this)\n" + printf "\n" + printf "Example:\n" + printf " %s -b https://localhost:4443/ -f ssl/owner/cert.pem -p Password --package https://packages.linkeddatahub.com/skos/#this\n" "$0" +} + +hash curl 2>/dev/null || { echo >&2 "curl not on \$PATH. Aborting."; exit 1; } + +unknown=() +while [[ $# -gt 0 ]] +do + key="$1" + + case $key in + -b|--base) + base="$2" + shift # past argument + shift # past value + ;; + -f|--cert-pem-file) + cert_pem_file="$2" + shift # past argument + shift # past value + ;; + -p|--cert-password) + cert_password="$2" + shift # past argument + shift # past value + ;; + --proxy) + proxy="$2" + shift # past argument + shift # past value + ;; + --package) + package_uri="$2" + shift # past argument + shift # past value + ;; + *) # unknown option + unknown+=("$1") # save it in an array for later + shift # past argument + ;; + esac +done +set -- "${unknown[@]}" # restore args + +if [ -z "$base" ] ; then + print_usage + exit 1 +fi +if [ -z "$cert_pem_file" ] ; then + print_usage + exit 1 +fi +if [ -z "$cert_password" ] ; then + print_usage + exit 1 +fi +if [ -z "$package_uri" ] ; then + print_usage + exit 1 +fi + +# Convert base URL to admin base URL +admin_uri() { + local uri="$1" + echo "$uri" | sed 's|://|://admin.|' +} + +admin_base=$(admin_uri "$base") +target_url="${admin_base}install-package" + +if [ -n "$proxy" ]; then + admin_proxy=$(admin_uri "$proxy") + # rewrite target hostname to proxy hostname + url_host=$(echo "$target_url" | cut -d '/' -f 1,2,3) + proxy_host=$(echo "$admin_proxy" | cut -d '/' -f 1,2,3) + final_url="${target_url/$url_host/$proxy_host}" +else + final_url="$target_url" +fi + +# POST to install-package endpoint +curl -k -w "%{http_code}\n" -E "${cert_pem_file}":"${cert_password}" \ + -H "Accept: text/turtle" \ + -d "package-uri=${package_uri}" \ + "${final_url}" diff --git a/platform/datasets/admin.trig b/platform/datasets/admin.trig index 111123be6..448c921ff 100644 --- a/platform/datasets/admin.trig +++ b/platform/datasets/admin.trig @@ -755,6 +755,42 @@ WHERE } +### PACKAGES ### + +# ENDPOINTS + +<packages/install> +{ + + <packages/install> a foaf:Document ; + dct:title "Install package endpoint" . + +} + +<packages/uninstall> +{ + + <packages/uninstall> a foaf:Document ; + dct:title "Uninstall package endpoint" . + +} + +# CONTAINERS + +<packages/> +{ + + <packages/> a dh:Container ; + sioc:has_parent <> ; + dct:title "Packages" ; + dct:description "Manage installed packages" ; + rdf:_1 <packages/#select-children> . + + <packages/#select-children> a ldh:Object ; + rdf:value ldh:ChildrenView . + +} + ### ONTOLOGIES ### # CONTAINERS diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 986c5e3c7..e177c6944 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -723,6 +723,7 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType BuiltinPersonalities.model.add(EndUserApplication.class, new com.atomgraph.linkeddatahub.apps.model.end_user.impl.ApplicationImplementation()); BuiltinPersonalities.model.add(com.atomgraph.linkeddatahub.apps.model.Application.class, new com.atomgraph.linkeddatahub.apps.model.impl.ApplicationImplementation()); BuiltinPersonalities.model.add(com.atomgraph.linkeddatahub.apps.model.Dataset.class, new com.atomgraph.linkeddatahub.apps.model.impl.DatasetImplementation()); + BuiltinPersonalities.model.add(com.atomgraph.linkeddatahub.apps.model.Package.class, new com.atomgraph.linkeddatahub.apps.model.impl.PackageImplementation()); BuiltinPersonalities.model.add(Service.class, new com.atomgraph.linkeddatahub.model.impl.ServiceImplementation(noCertClient, mediaTypes, maxGetRequestSize)); BuiltinPersonalities.model.add(Import.class, ImportImpl.factory); BuiltinPersonalities.model.add(RDFImport.class, RDFImportImpl.factory); diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/Application.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Application.java index 699066916..1832b7ad9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/apps/model/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Application.java @@ -30,11 +30,6 @@ public interface Application extends Resource, com.atomgraph.core.model.Application { - /** - * The relative path of the content-addressed file container. - */ - public static final String UPLOADS_PATH = "uploads"; - /** * Returns the application's namespace ontology. * @@ -108,9 +103,16 @@ public interface Application extends Resource, com.atomgraph.core.model.Applicat /** * Returns frontend proxy's cache URI resource. - * + * * @return RDF resource */ Resource getFrontendProxy(); - + + /** + * Returns the set of packages imported by this application. + * + * @return set of package resources + */ + java.util.Set<Resource> getImportedPackages(); + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java new file mode 100644 index 000000000..3c3cca52f --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java @@ -0,0 +1,46 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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.apps.model; + +import org.apache.jena.rdf.model.Resource; + +/** + * A LinkedDataHub package containing an ontology and optional XSLT stylesheet. + * Packages provide reusable vocabulary support with custom templates and rendering. + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public interface Package extends Resource +{ + + /** + * Returns the package's ontology resource. + * The ontology file (ns.ttl) contains RDF vocabulary classes/properties and template blocks. + * + * @return ontology resource, or null if not specified + */ + Resource getOntology(); + + /** + * Returns the package's stylesheet resource. + * The stylesheet file (layout.xsl) contains XSLT templates for custom rendering. + * + * @return stylesheet resource, or null if not specified + */ + Resource getStylesheet(); + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java index 043b18d90..6abddb282 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/ApplicationImpl.java @@ -26,9 +26,12 @@ import org.apache.jena.enhanced.EnhGraph; import org.apache.jena.graph.Node; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.StmtIterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; +import java.util.HashSet; +import java.util.Set; import org.apache.jena.rdf.model.Statement; import org.apache.jena.rdf.model.impl.ResourceImpl; @@ -117,9 +120,9 @@ public Resource getFrontendProxy() public boolean isReadAllowed() { Statement stmt = getProperty(LAPP.allowRead); - + if (stmt != null) return stmt.getBoolean(); - + return false; } @@ -128,4 +131,26 @@ public UriBuilder getUriBuilder() { return UriBuilder.fromUri(getOriginURI()); } + + @Override + public Set<Resource> getImportedPackages() + { + Set<Resource> packages = new HashSet<>(); + StmtIterator it = listProperties(LDH.importPackage); + try + { + while (it.hasNext()) + { + Statement stmt = it.next(); + if (stmt.getObject().isResource()) + packages.add(stmt.getResource()); + } + } + finally + { + it.close(); + } + return packages; + } + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java new file mode 100644 index 000000000..ec5ec3aac --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java @@ -0,0 +1,59 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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.apps.model.impl; + +import com.atomgraph.client.vocabulary.AC; +import com.atomgraph.linkeddatahub.apps.model.Package; +import com.atomgraph.server.vocabulary.LDT; +import org.apache.jena.enhanced.EnhGraph; +import org.apache.jena.graph.Node; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.impl.ResourceImpl; + + +/** + * LinkedDataHub package implementation. + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class PackageImpl extends ResourceImpl implements Package +{ + + /** + * Constructs instance from node and graph. + * + * @param n node + * @param g graph + */ + public PackageImpl(Node n, EnhGraph g) + { + super(n, g); + } + + @Override + public Resource getOntology() + { + return getPropertyResourceValue(LDT.ontology); + } + + @Override + public Resource getStylesheet() + { + return getPropertyResourceValue(AC.stylesheet); + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImplementation.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImplementation.java new file mode 100644 index 000000000..09ebb5e9d --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImplementation.java @@ -0,0 +1,56 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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.apps.model.impl; + +import com.atomgraph.linkeddatahub.vocabulary.LAPP; +import org.apache.jena.enhanced.EnhGraph; +import org.apache.jena.enhanced.EnhNode; +import org.apache.jena.enhanced.Implementation; +import org.apache.jena.graph.Node; +import org.apache.jena.ontology.ConversionException; +import org.apache.jena.vocabulary.RDF; + +/** + * Jena's implementation factory for Package. + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class PackageImplementation extends Implementation +{ + + @Override + public EnhNode wrap(Node node, EnhGraph enhGraph) + { + if (canWrap(node, enhGraph)) + { + return new PackageImpl(node, enhGraph); + } + else + { + throw new ConversionException("Cannot convert node " + node.toString() + " to Package: it does not have rdf:type lapp:Package"); + } + } + + @Override + public boolean canWrap(Node node, EnhGraph eg) + { + if (eg == null) throw new IllegalArgumentException("EnhGraph cannot be null"); + + return eg.asGraph().contains(node, RDF.type.asNode(), LAPP.Package.asNode()); + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java new file mode 100644 index 000000000..3e186d8ea --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java @@ -0,0 +1,304 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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.admin.pkg; + +import com.atomgraph.linkeddatahub.apps.model.AdminApplication; +import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; +import com.atomgraph.linkeddatahub.client.LinkedDataClient; +import com.atomgraph.linkeddatahub.server.model.impl.PackageManager; +import com.atomgraph.linkeddatahub.server.util.XsltMasterUpdater; +import jakarta.inject.Inject; +import jakarta.servlet.ServletContext; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * JAX-RS resource that installs a LinkedDataHub package. + * Package installation involves: + * 1. Fetching package metadata + * 2. Downloading package ontology (ns.ttl) and posting to namespace graph + * 3. Downloading package stylesheet (layout.xsl) and saving to /static/packages/ + * 4. Regenerating application master stylesheet + * 5. Adding ldh:import triple to application + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class Install +{ + private static final Logger log = LoggerFactory.getLogger(Install.class); + + private final com.atomgraph.linkeddatahub.apps.model.Application application; + private final com.atomgraph.linkeddatahub.Application system; + private final PackageManager packageManager = new PackageManager(); + + @Context ServletContext servletContext; + + /** + * Constructs endpoint. + * + * @param application matched application (admin app) + * @param system system application + */ + @Inject + public Install(com.atomgraph.linkeddatahub.apps.model.Application application, + com.atomgraph.linkeddatahub.Application system) + { + this.application = application; + this.system = system; + } + + /** + * Installs a package into the current dataspace. + * + * @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this) + * @param referer the referring URL + * @return JAX-RS response + */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("Referer") URI referer) + { + if (packageURI == null) throw new BadRequestException("Package URI not specified"); + + try + { + EndUserApplication endUserApp = getApplication().as(AdminApplication.class).getEndUserApplication(); + + if (log.isInfoEnabled()) log.info("Installing package: {}", packageURI); + + // 1. Fetch package + com.atomgraph.linkeddatahub.apps.model.Package pkg = getPackageManager().getPackage(packageURI); + if (pkg == null) throw new BadRequestException("Package not found: " + packageURI); + + Resource ontology = pkg.getOntology(); + Resource stylesheet = pkg.getStylesheet(); + + if (ontology == null) throw new BadRequestException("Package ontology not found"); + + URI ontologyURI = URI.create(ontology.getURI()); + URI stylesheetURI = (stylesheet != null) ? URI.create(stylesheet.getURI()) : null; + + String packagePath = getPackageManager().uriToPath(packageURI); + + // 2. Download and install ontology + if (log.isDebugEnabled()) log.debug("Downloading package ontology from: {}", ontologyURI); + Model ontologyModel = downloadOntology(ontologyURI); + installOntology(endUserApp, ontologyModel); + + // 3. Download and install stylesheet if present + if (stylesheetURI != null) + { + if (log.isDebugEnabled()) log.debug("Downloading package stylesheet from: {}", stylesheetURI); + String stylesheetContent = downloadStylesheet(stylesheetURI); + installStylesheet(packagePath, stylesheetContent); + } + + // 4. Regenerate master stylesheet + regenerateMasterStylesheet(endUserApp, packagePath); + + // 5. Add ldh:import triple to application (in system.trig) + addImportToApplication(endUserApp, packageURI); + + if (log.isInfoEnabled()) log.info("Successfully installed package: {}", packageURI); + + // Redirect back to referer or application base + URI redirectURI = (referer != null) ? referer : endUserApp.getBaseURI(); + return Response.seeOther(redirectURI).build(); + } + catch (BadRequestException | IOException e) + { + log.error("Failed to install package: {}", packageURI, e); + throw new InternalServerErrorException("Package installation failed: " + e.getMessage(), e); + } + } + + /** + * Downloads RDF from a URI using LinkedDataClient. + */ + private Model downloadOntology(URI uri) throws IOException + { + if (log.isDebugEnabled()) log.debug("Downloading RDF from: {}", uri); + + LinkedDataClient ldc = LinkedDataClient.create(getSystem().getClient(), getSystem().getMediaTypes()); + return ldc.getModel(uri.toString()); + } + + /** + * Downloads XSLT stylesheet content from a URI using Jersey Client. + * Prioritizes text/xsl, falls back to text/*. + */ + private String downloadStylesheet(URI uri) throws IOException + { + if (log.isDebugEnabled()) log.debug("Downloading XSLT stylesheet from: {}", uri); + + WebTarget target = getClient().target(uri); + // Prioritize text/xsl (q=1.0), then any text/* (q=0.8) + try (Response response = target.request("text/xsl", "text/*;q=0.8").get()) + { + if (!response.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)) + { + throw new IOException("Failed to download XSLT from " + uri + ": " + response.getStatus()); + } + + return response.readEntity(String.class); + } + } + + /** + * Installs ontology by POSTing to namespace graph. + */ + private void installOntology(EndUserApplication app, Model ontologyModel) throws IOException + { + if (log.isDebugEnabled()) log.debug("Posting package ontology to namespace graph"); + + // POST to admin namespace graph + AdminApplication adminApp = app.getAdminApplication(); + String namespaceGraphURI = UriBuilder.fromUri(adminApp.getBaseURI()).path("model/ontologies/namespace").build().toString(); + + // Use Graph Store Protocol to add ontology to namespace graph + adminApp.getService().getGraphStoreClient().add(namespaceGraphURI, ontologyModel); + } + + /** + * Installs stylesheet to /static/<package-path>/layout.xsl + */ + private void installStylesheet(String packagePath, String stylesheetContent) throws IOException + { + Path staticDir = Paths.get(getServletContext().getRealPath("/static")); + Path packageDir = staticDir.resolve(packagePath); + Files.createDirectories(packageDir); + + Path stylesheetFile = packageDir.resolve("layout.xsl"); + Files.writeString(stylesheetFile, stylesheetContent); + + if (log.isDebugEnabled()) log.debug("Installed package stylesheet at: {}", stylesheetFile); + } + + /** + * Regenerates master stylesheet for the application. + */ + private void regenerateMasterStylesheet(EndUserApplication app, String newPackagePath) throws IOException + { + // Get all currently installed packages + Set<Resource> packages = app.getImportedPackages(); + List<String> packagePaths = new ArrayList<>(); + + for (Resource pkg : packages) + { + packagePaths.add(getPackageManager().uriToPath(pkg.getURI())); + } + + // Add the new package + if (!packagePaths.contains(newPackagePath)) + { + packagePaths.add(newPackagePath); + } + + // Regenerate master stylesheet + String hostname = app.getBaseURI().getHost(); + XsltMasterUpdater updater = new XsltMasterUpdater(getServletContext()); + updater.regenerateMasterStylesheet(hostname, packagePaths); + } + + /** + * Adds ldh:import triple to the end-user application resource. + */ + private void addImportToApplication(EndUserApplication app, String packageURI) + { + // This would need to modify system.trig via SPARQL UPDATE + // For now, log a warning that this needs manual configuration + if (log.isWarnEnabled()) + { + log.warn("TODO: Add ldh:import triple to application. Manual edit required:"); + log.warn(" <{}> ldh:import <{}> .", app.getURI(), packageURI); + } + } + + /** + * Returns the current application. + * + * @return application resource + */ + public com.atomgraph.linkeddatahub.apps.model.Application getApplication() + { + return application; + } + + /** + * Returns the system application. + * + * @return system application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + + /** + * Returns Jersey HTTP client. + * + * @return HTTP client + */ + public Client getClient() + { + return getSystem().getClient(); + } + + /** + * Returns servlet context. + * + * @return servlet context + */ + public ServletContext getServletContext() + { + return servletContext; + } + + /** + * Returns package manager. + * + * @return package manager + */ + public PackageManager getPackageManager() + { + return packageManager; + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java new file mode 100644 index 000000000..4e1aefc9f --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java @@ -0,0 +1,237 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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.admin.pkg; + +import com.atomgraph.linkeddatahub.apps.model.AdminApplication; +import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; +import com.atomgraph.linkeddatahub.server.model.impl.PackageManager; +import com.atomgraph.linkeddatahub.server.util.XsltMasterUpdater; +import jakarta.inject.Inject; +import jakarta.servlet.ServletContext; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.update.UpdateExecutionFactory; +import org.apache.jena.update.UpdateFactory; +import org.apache.jena.update.UpdateRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * JAX-RS resource that uninstalls a LinkedDataHub package. + * Package uninstallation involves: + * 1. Removing package ontology triples from namespace graph + * 2. Deleting package stylesheet from /static/packages/ + * 3. Regenerating application master stylesheet + * 4. Removing ldh:import triple from application + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class Uninstall +{ + private static final Logger log = LoggerFactory.getLogger(Uninstall.class); + + private final com.atomgraph.linkeddatahub.apps.model.Application application; + private final PackageManager packageManager = new PackageManager(); + + @Context ServletContext servletContext; + + /** + * Constructs endpoint. + * + * @param application matched application (admin app) + */ + @Inject + public Uninstall(com.atomgraph.linkeddatahub.apps.model.Application application) + { + this.application = application; + } + + /** + * Uninstalls a package from the current dataspace. + * + * @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this) + * @param referer the referring URL + * @return JAX-RS response + */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("Referer") URI referer) + { + if (packageURI == null) throw new BadRequestException("Package URI not specified"); + + try + { + EndUserApplication endUserApp = getApplication().as(AdminApplication.class).getEndUserApplication(); + + if (log.isInfoEnabled()) log.info("Uninstalling package: {}", packageURI); + + String packagePath = getPackageManager().uriToPath(packageURI); + + // 1. Remove ontology triples from namespace graph + uninstallOntology(endUserApp, packagePath); + + // 2. Delete stylesheet from /static/<package-path>/ + uninstallStylesheet(packagePath); + + // 3. Regenerate master stylesheet + regenerateMasterStylesheet(endUserApp, packagePath); + + // 4. Remove ldh:import triple from application + removeImportFromApplication(endUserApp, packageURI); + + if (log.isInfoEnabled()) log.info("Successfully uninstalled package: {}", packageURI); + + // Redirect back to referer or application base + URI redirectURI = (referer != null) ? referer : endUserApp.getBaseURI(); + return Response.seeOther(redirectURI).build(); + } + catch (Exception e) + { + log.error("Failed to uninstall package: {}", packageURI, e); + throw new jakarta.ws.rs.InternalServerErrorException("Package uninstallation failed: " + e.getMessage(), e); + } + } + + /** + * Uninstalls ontology by removing triples from namespace graph. + * This is a simplified version - a real implementation would track which triples belong to which package. + */ + private void uninstallOntology(EndUserApplication app, String packagePath) throws IOException + { + if (log.isWarnEnabled()) + { + log.warn("TODO: Remove package ontology triples from namespace graph"); + log.warn(" This requires tracking which triples belong to package: {}", packagePath); + } + // For now, we don't remove ontology triples as it's complex to track ownership + // A future enhancement could use named graphs per package + } + + /** + * Deletes stylesheet from /static/<package-path>/ + */ + private void uninstallStylesheet(String packagePath) throws IOException + { + Path staticDir = Paths.get(getServletContext().getRealPath("/static")); + Path packageDir = staticDir.resolve(packagePath); + + if (Files.exists(packageDir)) + { + // Delete layout.xsl + Path stylesheetFile = packageDir.resolve("layout.xsl"); + if (Files.exists(stylesheetFile)) + { + Files.delete(stylesheetFile); + if (log.isDebugEnabled()) log.debug("Deleted package stylesheet: {}", stylesheetFile); + } + + // Delete directory if empty + if (Files.list(packageDir).count() == 0) + { + Files.delete(packageDir); + if (log.isDebugEnabled()) log.debug("Deleted package directory: {}", packageDir); + } + } + } + + /** + * Regenerates master stylesheet for the application without the uninstalled package. + */ + private void regenerateMasterStylesheet(EndUserApplication app, String removedPackagePath) throws IOException + { + // Get all currently installed packages + Set<Resource> packages = app.getImportedPackages(); + List<String> packagePaths = new ArrayList<>(); + + for (Resource pkg : packages) + { + String pkgPath = getPackageManager().uriToPath(pkg.getURI()); + // Exclude the package being removed + if (!pkgPath.equals(removedPackagePath)) + { + packagePaths.add(pkgPath); + } + } + + // Regenerate master stylesheet + String hostname = app.getBaseURI().getHost(); + XsltMasterUpdater updater = new XsltMasterUpdater(getServletContext()); + updater.regenerateMasterStylesheet(hostname, packagePaths); + } + + /** + * Removes ldh:import triple from the end-user application resource. + */ + private void removeImportFromApplication(EndUserApplication app, String packageURI) + { + // This would need to modify system.trig via SPARQL UPDATE + // For now, log a warning that this needs manual configuration + if (log.isWarnEnabled()) + { + log.warn("TODO: Remove ldh:import triple from application. Manual edit required:"); + log.warn(" DELETE DATA {{ <{}> ldh:import <{}> }}", app.getURI(), packageURI); + } + } + + /** + * Returns the current application. + * + * @return application resource + */ + public com.atomgraph.linkeddatahub.apps.model.Application getApplication() + { + return application; + } + + /** + * Returns servlet context. + * + * @return servlet context + */ + public ServletContext getServletContext() + { + return servletContext; + } + + /** + * Returns package manager. + * + * @return package manager + */ + public PackageManager getPackageManager() + { + return packageManager; + } + +} 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 91667c801..223c7b2cb 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 @@ -23,6 +23,8 @@ import com.atomgraph.linkeddatahub.resource.Namespace; import com.atomgraph.linkeddatahub.resource.Transform; import com.atomgraph.linkeddatahub.resource.admin.Clear; +import com.atomgraph.linkeddatahub.resource.admin.pkg.Install; +import com.atomgraph.linkeddatahub.resource.admin.pkg.Uninstall; import com.atomgraph.linkeddatahub.resource.admin.SignUp; import com.atomgraph.linkeddatahub.resource.Graph; import com.atomgraph.linkeddatahub.resource.acl.Access; @@ -216,7 +218,7 @@ public Class getGenerateEndpoint() /** * Returns the endpoint that allows clearing ontologies from cache by URI. - * + * * @return endpoint resource */ @Path("clear") @@ -225,6 +227,28 @@ public Class getClearEndpoint() return getProxyClass().orElse(Clear.class); } + /** + * Returns the endpoint for installing LinkedDataHub packages. + * + * @return endpoint resource + */ + @Path("packages/install") + public Class getInstallPackageEndpoint() + { + return getProxyClass().orElse(Install.class); + } + + /** + * Returns the endpoint for uninstalling LinkedDataHub packages. + * + * @return endpoint resource + */ + @Path("packages/uninstall") + public Class getUninstallPackageEndpoint() + { + return getProxyClass().orElse(Uninstall.class); + } + /** * Returns the default JAX-RS resource class. * diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java index 6dc3c2f10..1c25cd2a2 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java @@ -18,7 +18,6 @@ import com.atomgraph.core.MediaTypes; import com.atomgraph.core.riot.lang.RDFPostReader; -import static com.atomgraph.linkeddatahub.apps.model.Application.UPLOADS_PATH; import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.server.security.AgentContext; import java.net.URI; @@ -57,6 +56,11 @@ public abstract class GraphStoreImpl extends com.atomgraph.core.model.impl.Graph { private static final Logger log = LoggerFactory.getLogger(GraphStoreImpl.class); + + /** + * The relative path of the content-addressed file container. + */ + public static final String UPLOADS_PATH = "uploads"; private final UriInfo uriInfo; private final com.atomgraph.linkeddatahub.apps.model.Application application; diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java new file mode 100644 index 000000000..258b24921 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java @@ -0,0 +1,170 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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.apps.model.Application; +import com.atomgraph.linkeddatahub.apps.model.Package; +import com.atomgraph.linkeddatahub.vocabulary.LDH; +import jakarta.ws.rs.InternalServerErrorException; +import org.apache.jena.rdf.model.*; +import org.apache.jena.riot.RDFDataMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.*; + +/** + * Package manager for LinkedDataHub packages. + * Handles loading package metadata and content (ontologies and stylesheets). + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class PackageManager +{ + private static final Logger log = LoggerFactory.getLogger(PackageManager.class); + + /** + * Get the list of package URIs imported by an application. + * + * @param app the application + * @return set of package URIs + */ + public Set<Resource> getImportedPackages(Application app) + { + if (app == null) return Collections.emptySet(); + + Set<Resource> packages = new HashSet<>(); + StmtIterator it = app.listProperties(LDH.importPackage); + try + { + while (it.hasNext()) + { + Statement stmt = it.next(); + if (stmt.getObject().isResource()) + packages.add(stmt.getResource()); + } + } + finally + { + it.close(); + } + + return packages; + } + + /** + * Load package from its URI. + * Package metadata is expected to be available as Linked Data. + * + * @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this) + * @return Package instance + * @throws InternalServerErrorException if package cannot be loaded + */ + public Package getPackage(String packageURI) + { + try + { + if (log.isDebugEnabled()) log.debug("Loading package from: {}", packageURI); + Model model = ModelFactory.createDefaultModel(); + RDFDataMgr.read(model, packageURI); + + return model.getResource(packageURI).as(Package.class); + } + catch (Exception e) + { + log.error("Failed to load package from: {}", packageURI, e); + throw new InternalServerErrorException("Failed to load package from: " + packageURI, e); + } + } + + /** + * Load a package's ontology from its ns.ttl URI. + * + * @param ontologyURI the URI of the package's ontology file + * @return RDF model containing the ontology + * @throws IllegalArgumentException if ontologyURI is null + * @throws InternalServerErrorException if ontology cannot be loaded + */ + public Model loadPackageOntology(URI ontologyURI) + { + if (ontologyURI == null) + throw new IllegalArgumentException("Package ontology URI cannot be null"); + + String uriString = ontologyURI.toString(); + + try + { + if (log.isDebugEnabled()) log.debug("Loading package ontology from: {}", uriString); + Model model = ModelFactory.createDefaultModel(); + RDFDataMgr.read(model, uriString); + return model; + } + catch (Exception e) + { + log.error("Failed to load package ontology from: {}", uriString, e); + throw new InternalServerErrorException("Failed to load package ontology from: " + uriString, e); + } + } + + /** + * Converts a package URI to a filesystem path by reversing hostname components. + * Example: https://packages.linkeddatahub.com/skos/#this -> com/linkeddatahub/packages/skos + * + * @param packageURI the package URI + * @return filesystem path relative to static directory + * @throws IllegalArgumentException if URI is invalid + */ + public String uriToPath(String packageURI) + { + if (packageURI == null) + throw new IllegalArgumentException("Package URI cannot be null"); + + try + { + URI uri = URI.create(packageURI); + String host = uri.getHost(); + String path = uri.getPath(); + + if (host == null) + throw new IllegalArgumentException("Package URI must have a host: " + packageURI); + + // Reverse hostname components: packages.linkeddatahub.com -> com/linkeddatahub/packages + String[] hostParts = host.split("\\."); + StringBuilder reversedHost = new StringBuilder(); + for (int i = hostParts.length - 1; i >= 0; i--) + { + reversedHost.append(hostParts[i]); + if (i > 0) reversedHost.append("/"); + } + + // Append path without leading/trailing slashes and fragment + if (path != null && !path.isEmpty() && !path.equals("/")) + { + String cleanPath = path.replaceAll("^/+|/+$", ""); // Remove leading/trailing slashes + return reversedHost + "/" + cleanPath; + } + + return reversedHost.toString(); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException("Invalid package URI: " + packageURI, e); + } + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/XsltMasterUpdater.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/XsltMasterUpdater.java new file mode 100644 index 000000000..0804ce7b4 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/XsltMasterUpdater.java @@ -0,0 +1,244 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import jakarta.servlet.ServletContext; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Updates master XSLT stylesheets with package import chains. + * Writes master stylesheets to the webapp's /static/ directory. + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class XsltMasterUpdater +{ + private static final Logger log = LoggerFactory.getLogger(XsltMasterUpdater.class); + + private static final String XSL_NS = "http://www.w3.org/1999/XSL/Transform"; + private static final String XS_NS = "http://www.w3.org/2001/XMLSchema"; + + private final ServletContext servletContext; + + /** + * Constructs updater with servlet context. + * + * @param servletContext the servlet context + */ + public XsltMasterUpdater(ServletContext servletContext) + { + this.servletContext = servletContext; + } + + /** + * Regenerates the master stylesheet for an application hostname. + * The master stylesheet must exist at /static/<hostname>/layout.xsl. + * This method loads it and adds/updates xsl:import elements for packages. + * + * @param hostname the application hostname (e.g., "localhost") + * @param packagePaths list of package paths to import (e.g., ["com/linkeddatahub/packages/skos"]) + * @throws IOException if file operations fail + */ + public void regenerateMasterStylesheet(String hostname, List<String> packagePaths) throws IOException + { + if (hostname == null) throw new IllegalArgumentException("Hostname cannot be null"); + + try + { + Path staticDir = getStaticPath(); + Path hostnameDir = staticDir.resolve(hostname); + Path masterFile = hostnameDir.resolve("layout.xsl"); + + // Master stylesheet must exist + if (!Files.exists(masterFile)) + { + throw new jakarta.ws.rs.InternalServerErrorException("Master stylesheet does not exist: " + masterFile); + } + + // Load existing master stylesheet + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + + if (log.isDebugEnabled()) log.debug("Loading master stylesheet: {}", masterFile); + Document doc = builder.parse(masterFile.toFile()); + + // Get stylesheet root element + Element stylesheet = doc.getDocumentElement(); + if (!stylesheet.getLocalName().equals("stylesheet") || !XSL_NS.equals(stylesheet.getNamespaceURI())) + { + throw new IllegalStateException("Root element must be xsl:stylesheet"); + } + + // Remove all existing xsl:import elements for packages + removePackageImports(stylesheet); + + // Add xsl:import elements for packages (after system import, before everything else) + Element systemImport = findSystemImport(stylesheet); + Node insertAfter = systemImport; + + if (packagePaths != null && !packagePaths.isEmpty()) + { + for (String packagePath : packagePaths) + { + Element importElement = doc.createElementNS(XSL_NS, "xsl:import"); + importElement.setAttribute("href", "../" + packagePath + "/layout.xsl"); + + // Add comment + org.w3c.dom.Comment comment = doc.createComment(" Package: " + packagePath + " "); + if (insertAfter.getNextSibling() != null) + { + stylesheet.insertBefore(comment, insertAfter.getNextSibling()); + stylesheet.insertBefore(importElement, insertAfter.getNextSibling()); + } + else + { + stylesheet.appendChild(comment); + stylesheet.appendChild(importElement); + } + insertAfter = importElement; + + if (log.isDebugEnabled()) log.debug("Added xsl:import for package: {}", packagePath); + } + } + + // Write to file + Files.createDirectories(hostnameDir); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(masterFile.toFile()); + transformer.transform(source, result); + + if (log.isDebugEnabled()) log.debug("Regenerated master stylesheet for hostname '{}' at: {}", hostname, masterFile); + } + catch (Exception e) + { + throw new IOException("Failed to regenerate master stylesheet", e); + } + } + + /** + * Finds the system stylesheet import element. + */ + private Element findSystemImport(Element stylesheet) + { + NodeList imports = stylesheet.getElementsByTagNameNS(XSL_NS, "import"); + for (int i = 0; i < imports.getLength(); i++) + { + Element importElem = (Element) imports.item(i); + String href = importElem.getAttribute("href"); + if (href.contains("/com/atomgraph/linkeddatahub/xsl/bootstrap/")) + { + return importElem; + } + } + throw new IllegalStateException("System stylesheet import not found"); + } + + /** + * Removes all xsl:import elements for packages. + * Identifies package imports by checking if they have a preceding comment containing "Package:". + */ + private void removePackageImports(Element stylesheet) + { + NodeList imports = stylesheet.getElementsByTagNameNS(XSL_NS, "import"); + List<Element> toRemove = new ArrayList<>(); + + for (int i = 0; i < imports.getLength(); i++) + { + Element importElem = (Element) imports.item(i); + String href = importElem.getAttribute("href"); + + // Check if this is a package import by looking for "../" prefix and preceding comment + if (href.startsWith("../") && !href.contains("/com/atomgraph/linkeddatahub/xsl/")) + { + // Verify it has a package comment + Node prev = importElem.getPreviousSibling(); + while (prev != null && prev.getNodeType() == Node.TEXT_NODE) + { + prev = prev.getPreviousSibling(); + } + if (prev != null && prev.getNodeType() == Node.COMMENT_NODE) + { + String commentText = prev.getNodeValue(); + if (commentText.contains("Package:")) + { + toRemove.add(importElem); + } + } + } + } + + for (Element elem : toRemove) + { + // Remove preceding comment if it's a package comment + Node prev = elem.getPreviousSibling(); + while (prev != null && prev.getNodeType() == Node.TEXT_NODE) + { + prev = prev.getPreviousSibling(); + } + if (prev != null && prev.getNodeType() == Node.COMMENT_NODE) + { + String commentText = prev.getNodeValue(); + if (commentText.contains("Package:")) + { + stylesheet.removeChild(prev); + } + } + + stylesheet.removeChild(elem); + } + } + + /** + * Gets the path to the webapp's /static/ directory. + * + * @return path to static directory + */ + private Path getStaticPath() + { + String realPath = servletContext.getRealPath("/static"); + if (realPath == null) + throw new IllegalStateException("Could not resolve real path for /static directory"); + return Paths.get(realPath); + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java index fe51bdd86..3161938c9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java @@ -63,7 +63,10 @@ public static String getURI() /** End-user application class */ public static final OntClass EndUserApplication = m_model.createClass( NS + "EndUserApplication" ); - + + /** Package class */ + public static final OntClass Package = m_model.createClass( NS + "Package" ); + /** Admin application class */ public static final ObjectProperty adminApplication = m_model.createObjectProperty( NS + "adminApplication" ); diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java index 555ffdce5..17414f681 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java @@ -106,7 +106,7 @@ public static String getURI() public static final ObjectProperty forShape = m_model.createObjectProperty( NS + "forShape" ); /** - * Graph URI property */ - //public static final ObjectProperty graph = m_model.createObjectProperty( NS + "graph" ); + * Import property - used to import packages into an application */ + public static final ObjectProperty importPackage = m_model.createObjectProperty( NS + "import" ); } diff --git a/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl b/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl index 77c6c15fe..819120f9b 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/lapp.ttl @@ -163,6 +163,13 @@ rdfs:label "Admin application constructor" ; rdfs:isDefinedBy : . +# package + +:Package a rdfs:Class, owl:Class ; + rdfs:label "Package" ; + rdfs:comment "A reusable package containing RDF ontology and optional XSLT stylesheet for vocabulary support" ; + rdfs:isDefinedBy : . + # CONSTRAINTS :StartsWithHTTPS a sp:Construct ; diff --git a/src/main/webapp/static/localhost/layout.xsl b/src/main/webapp/static/localhost/layout.xsl new file mode 100644 index 000000000..ceb6d2b13 --- /dev/null +++ b/src/main/webapp/static/localhost/layout.xsl @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet version="3.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + exclude-result-prefixes="xs"> + + <!-- System stylesheet (lowest priority) --> + <xsl:import href="../com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl"/> + + <!-- Package stylesheets will be added here by InstallPackage endpoint --> + +</xsl:stylesheet> From 865015f219c3f51c987d70e6e292dfe3f1970535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Sun, 30 Nov 2025 15:42:42 +0100 Subject: [PATCH 10/12] Removed `PackageManager` --- .../linkeddatahub/apps/model/Package.java | 8 + .../apps/model/impl/PackageImpl.java | 29 +++ .../resource/admin/pkg/Install.java | 50 +++--- .../resource/admin/pkg/Uninstall.java | 17 +- .../server/model/impl/PackageManager.java | 170 ------------------ .../linkeddatahub/server/util/UriPath.java | 76 ++++++++ 6 files changed, 146 insertions(+), 204 deletions(-) delete mode 100644 src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java create mode 100644 src/main/java/com/atomgraph/linkeddatahub/server/util/UriPath.java diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java index 3c3cca52f..4ad8b71e5 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java @@ -43,4 +43,12 @@ public interface Package extends Resource */ Resource getStylesheet(); + /** + * Returns the packages imported by this package. + * Packages can transitively import other packages via ldh:import property. + * + * @return set of imported package resources + */ + java.util.Set<Resource> getImportedPackages(); + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java index ec5ec3aac..a431ee262 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java @@ -18,12 +18,18 @@ import com.atomgraph.client.vocabulary.AC; import com.atomgraph.linkeddatahub.apps.model.Package; +import com.atomgraph.linkeddatahub.vocabulary.LDH; import com.atomgraph.server.vocabulary.LDT; import org.apache.jena.enhanced.EnhGraph; import org.apache.jena.graph.Node; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.rdf.model.impl.ResourceImpl; +import java.util.HashSet; +import java.util.Set; + /** * LinkedDataHub package implementation. @@ -56,4 +62,27 @@ public Resource getStylesheet() return getPropertyResourceValue(AC.stylesheet); } + @Override + public Set<Resource> getImportedPackages() + { + Set<Resource> packages = new HashSet<>(); + StmtIterator it = listProperties(LDH.importPackage); + + try + { + while (it.hasNext()) + { + Statement stmt = it.next(); + if (stmt.getObject().isResource()) + packages.add(stmt.getResource()); + } + } + finally + { + it.close(); + } + + return packages; + } + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java index 3e186d8ea..77ab794a5 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java @@ -19,7 +19,7 @@ import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.LinkedDataClient; -import com.atomgraph.linkeddatahub.server.model.impl.PackageManager; +import com.atomgraph.linkeddatahub.server.util.UriPath; import com.atomgraph.linkeddatahub.server.util.XsltMasterUpdater; import jakarta.inject.Inject; import jakarta.servlet.ServletContext; @@ -39,7 +39,6 @@ import org.apache.jena.rdf.model.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.io.IOException; import java.net.URI; import java.nio.file.Files; @@ -66,7 +65,6 @@ public class Install private final com.atomgraph.linkeddatahub.apps.model.Application application; private final com.atomgraph.linkeddatahub.Application system; - private final PackageManager packageManager = new PackageManager(); @Context ServletContext servletContext; @@ -104,7 +102,7 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam(" if (log.isInfoEnabled()) log.info("Installing package: {}", packageURI); // 1. Fetch package - com.atomgraph.linkeddatahub.apps.model.Package pkg = getPackageManager().getPackage(packageURI); + com.atomgraph.linkeddatahub.apps.model.Package pkg = getPackage(packageURI); if (pkg == null) throw new BadRequestException("Package not found: " + packageURI); Resource ontology = pkg.getOntology(); @@ -115,7 +113,7 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam(" URI ontologyURI = URI.create(ontology.getURI()); URI stylesheetURI = (stylesheet != null) ? URI.create(stylesheet.getURI()) : null; - String packagePath = getPackageManager().uriToPath(packageURI); + String packagePath = UriPath.convert(packageURI); // 2. Download and install ontology if (log.isDebugEnabled()) log.debug("Downloading package ontology from: {}", ontologyURI); @@ -149,6 +147,32 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam(" } } + /** + * Loads package metadata from its URI using LinkedDataClient. + * Package metadata is expected to be available as Linked Data. + * + * @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this) + * @return Package instance + * @throws InternalServerErrorException if package cannot be loaded + */ + private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String packageURI) + { + try + { + if (log.isDebugEnabled()) log.debug("Loading package from: {}", packageURI); + + LinkedDataClient ldc = LinkedDataClient.create(getSystem().getClient(), getSystem().getMediaTypes()); + Model model = ldc.getModel(packageURI); + + return model.getResource(packageURI).as(com.atomgraph.linkeddatahub.apps.model.Package.class); + } + catch (Exception e) + { + log.error("Failed to load package from: {}", packageURI, e); + throw new InternalServerErrorException("Failed to load package from: " + packageURI, e); + } + } + /** * Downloads RDF from a URI using LinkedDataClient. */ @@ -221,15 +245,11 @@ private void regenerateMasterStylesheet(EndUserApplication app, String newPackag List<String> packagePaths = new ArrayList<>(); for (Resource pkg : packages) - { - packagePaths.add(getPackageManager().uriToPath(pkg.getURI())); - } + packagePaths.add(UriPath.convert(pkg.getURI())); // Add the new package if (!packagePaths.contains(newPackagePath)) - { packagePaths.add(newPackagePath); - } // Regenerate master stylesheet String hostname = app.getBaseURI().getHost(); @@ -291,14 +311,4 @@ public ServletContext getServletContext() return servletContext; } - /** - * Returns package manager. - * - * @return package manager - */ - public PackageManager getPackageManager() - { - return packageManager; - } - } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java index 4e1aefc9f..1bc5b169f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java @@ -18,7 +18,7 @@ import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import com.atomgraph.linkeddatahub.server.model.impl.PackageManager; +import com.atomgraph.linkeddatahub.server.util.UriPath; import com.atomgraph.linkeddatahub.server.util.XsltMasterUpdater; import jakarta.inject.Inject; import jakarta.servlet.ServletContext; @@ -62,7 +62,6 @@ public class Uninstall private static final Logger log = LoggerFactory.getLogger(Uninstall.class); private final com.atomgraph.linkeddatahub.apps.model.Application application; - private final PackageManager packageManager = new PackageManager(); @Context ServletContext servletContext; @@ -96,7 +95,7 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam(" if (log.isInfoEnabled()) log.info("Uninstalling package: {}", packageURI); - String packagePath = getPackageManager().uriToPath(packageURI); + String packagePath = UriPath.convert(packageURI); // 1. Remove ontology triples from namespace graph uninstallOntology(endUserApp, packagePath); @@ -176,7 +175,7 @@ private void regenerateMasterStylesheet(EndUserApplication app, String removedPa for (Resource pkg : packages) { - String pkgPath = getPackageManager().uriToPath(pkg.getURI()); + String pkgPath = UriPath.convert(pkg.getURI()); // Exclude the package being removed if (!pkgPath.equals(removedPackagePath)) { @@ -224,14 +223,4 @@ public ServletContext getServletContext() return servletContext; } - /** - * Returns package manager. - * - * @return package manager - */ - public PackageManager getPackageManager() - { - return packageManager; - } - } diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java deleted file mode 100644 index 258b24921..000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/PackageManager.java +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> - * - * 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.apps.model.Application; -import com.atomgraph.linkeddatahub.apps.model.Package; -import com.atomgraph.linkeddatahub.vocabulary.LDH; -import jakarta.ws.rs.InternalServerErrorException; -import org.apache.jena.rdf.model.*; -import org.apache.jena.riot.RDFDataMgr; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.util.*; - -/** - * Package manager for LinkedDataHub packages. - * Handles loading package metadata and content (ontologies and stylesheets). - * - * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} - */ -public class PackageManager -{ - private static final Logger log = LoggerFactory.getLogger(PackageManager.class); - - /** - * Get the list of package URIs imported by an application. - * - * @param app the application - * @return set of package URIs - */ - public Set<Resource> getImportedPackages(Application app) - { - if (app == null) return Collections.emptySet(); - - Set<Resource> packages = new HashSet<>(); - StmtIterator it = app.listProperties(LDH.importPackage); - try - { - while (it.hasNext()) - { - Statement stmt = it.next(); - if (stmt.getObject().isResource()) - packages.add(stmt.getResource()); - } - } - finally - { - it.close(); - } - - return packages; - } - - /** - * Load package from its URI. - * Package metadata is expected to be available as Linked Data. - * - * @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this) - * @return Package instance - * @throws InternalServerErrorException if package cannot be loaded - */ - public Package getPackage(String packageURI) - { - try - { - if (log.isDebugEnabled()) log.debug("Loading package from: {}", packageURI); - Model model = ModelFactory.createDefaultModel(); - RDFDataMgr.read(model, packageURI); - - return model.getResource(packageURI).as(Package.class); - } - catch (Exception e) - { - log.error("Failed to load package from: {}", packageURI, e); - throw new InternalServerErrorException("Failed to load package from: " + packageURI, e); - } - } - - /** - * Load a package's ontology from its ns.ttl URI. - * - * @param ontologyURI the URI of the package's ontology file - * @return RDF model containing the ontology - * @throws IllegalArgumentException if ontologyURI is null - * @throws InternalServerErrorException if ontology cannot be loaded - */ - public Model loadPackageOntology(URI ontologyURI) - { - if (ontologyURI == null) - throw new IllegalArgumentException("Package ontology URI cannot be null"); - - String uriString = ontologyURI.toString(); - - try - { - if (log.isDebugEnabled()) log.debug("Loading package ontology from: {}", uriString); - Model model = ModelFactory.createDefaultModel(); - RDFDataMgr.read(model, uriString); - return model; - } - catch (Exception e) - { - log.error("Failed to load package ontology from: {}", uriString, e); - throw new InternalServerErrorException("Failed to load package ontology from: " + uriString, e); - } - } - - /** - * Converts a package URI to a filesystem path by reversing hostname components. - * Example: https://packages.linkeddatahub.com/skos/#this -> com/linkeddatahub/packages/skos - * - * @param packageURI the package URI - * @return filesystem path relative to static directory - * @throws IllegalArgumentException if URI is invalid - */ - public String uriToPath(String packageURI) - { - if (packageURI == null) - throw new IllegalArgumentException("Package URI cannot be null"); - - try - { - URI uri = URI.create(packageURI); - String host = uri.getHost(); - String path = uri.getPath(); - - if (host == null) - throw new IllegalArgumentException("Package URI must have a host: " + packageURI); - - // Reverse hostname components: packages.linkeddatahub.com -> com/linkeddatahub/packages - String[] hostParts = host.split("\\."); - StringBuilder reversedHost = new StringBuilder(); - for (int i = hostParts.length - 1; i >= 0; i--) - { - reversedHost.append(hostParts[i]); - if (i > 0) reversedHost.append("/"); - } - - // Append path without leading/trailing slashes and fragment - if (path != null && !path.isEmpty() && !path.equals("/")) - { - String cleanPath = path.replaceAll("^/+|/+$", ""); // Remove leading/trailing slashes - return reversedHost + "/" + cleanPath; - } - - return reversedHost.toString(); - } - catch (IllegalArgumentException e) - { - throw new IllegalArgumentException("Invalid package URI: " + packageURI, e); - } - } - -} diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/UriPath.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/UriPath.java new file mode 100644 index 000000000..aa59c9273 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/UriPath.java @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.server.util; + +import java.net.URI; + +/** + * Utility for converting URIs to filesystem paths. + * Reverses hostname components following Java package naming conventions. + * + * @author Martynas Jusevičius {@literal <martynas@atomgraph.com>} + */ +public class UriPath +{ + + /** + * Converts a URI to a filesystem path by reversing hostname components. + * Example: https://packages.linkeddatahub.com/skos/#this -> com/linkeddatahub/packages/skos + * + * @param uri the URI string + * @return filesystem path relative to static directory + * @throws IllegalArgumentException if URI is invalid + */ + public static String convert(String uri) + { + if (uri == null) + throw new IllegalArgumentException("URI cannot be null"); + + try + { + URI uriObj = URI.create(uri); + String host = uriObj.getHost(); + String path = uriObj.getPath(); + + if (host == null) + throw new IllegalArgumentException("URI must have a host: " + uri); + + // Reverse hostname components: packages.linkeddatahub.com -> com/linkeddatahub/packages + String[] hostParts = host.split("\\."); + StringBuilder reversedHost = new StringBuilder(); + for (int i = hostParts.length - 1; i >= 0; i--) + { + reversedHost.append(hostParts[i]); + if (i > 0) reversedHost.append("/"); + } + + // Append path without leading/trailing slashes and fragment + if (path != null && !path.isEmpty() && !path.equals("/")) + { + String cleanPath = path.replaceAll("^/+|/+$", ""); // Remove leading/trailing slashes + return reversedHost + "/" + cleanPath; + } + + return reversedHost.toString(); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException("Invalid URI: " + uri, e); + } + } + +} From 48f4e9ea1b1da026d0ff02a8582d39115035d18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Sun, 30 Nov 2025 19:50:36 +0100 Subject: [PATCH 11/12] HTTP unit tests for packages --- .../admin/packages/install-package-400.sh | 16 +++++ .../admin/packages/install-package-401.sh | 16 +++++ .../admin/packages/install-package-404.sh | 18 +++++ http-tests/admin/packages/install-package.sh | 36 ++++++++++ .../packages/install-uninstall-package.sh | 65 +++++++++++++++++++ .../admin/packages/uninstall-package-400.sh | 16 +++++ .../admin/packages/uninstall-package.sh | 50 ++++++++++++++ .../resource/admin/pkg/Install.java | 11 +++- 8 files changed, 227 insertions(+), 1 deletion(-) create mode 100755 http-tests/admin/packages/install-package-400.sh create mode 100755 http-tests/admin/packages/install-package-401.sh create mode 100755 http-tests/admin/packages/install-package-404.sh create mode 100755 http-tests/admin/packages/install-package.sh create mode 100755 http-tests/admin/packages/install-uninstall-package.sh create mode 100755 http-tests/admin/packages/uninstall-package-400.sh create mode 100755 http-tests/admin/packages/uninstall-package.sh diff --git a/http-tests/admin/packages/install-package-400.sh b/http-tests/admin/packages/install-package-400.sh new file mode 100755 index 000000000..ddae0068a --- /dev/null +++ b/http-tests/admin/packages/install-package-400.sh @@ -0,0 +1,16 @@ +#!/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" + +# Missing package-uri parameter should return 400 Bad Request +curl -k -w "%{http_code}\n" -o /dev/null -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + "$ADMIN_BASE_URL"packages/install \ +| grep -q "$STATUS_BAD_REQUEST" diff --git a/http-tests/admin/packages/install-package-401.sh b/http-tests/admin/packages/install-package-401.sh new file mode 100755 index 000000000..c888a50ad --- /dev/null +++ b/http-tests/admin/packages/install-package-401.sh @@ -0,0 +1,16 @@ +#!/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" + +# Unauthorized access (without certificate) should return 401 Unauthorized +curl -k -w "%{http_code}\n" -o /dev/null -s \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=https://packages.linkeddatahub.com/skos/#this" \ + "$ADMIN_BASE_URL"packages/install \ +| grep -q "$STATUS_UNAUTHORIZED" diff --git a/http-tests/admin/packages/install-package-404.sh b/http-tests/admin/packages/install-package-404.sh new file mode 100755 index 000000000..47091ef5d --- /dev/null +++ b/http-tests/admin/packages/install-package-404.sh @@ -0,0 +1,18 @@ +#!/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" + +# Invalid/non-existent package URI should return 404 Not Found +# (the HTTP client error from the remote package server is re-thrown) +curl -k -w "%{http_code}\n" -o /dev/null -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=https://packages.linkeddatahub.com/nonexistent/#package" \ + "$ADMIN_BASE_URL"packages/install \ +| grep -q "$STATUS_NOT_FOUND" diff --git a/http-tests/admin/packages/install-package.sh b/http-tests/admin/packages/install-package.sh new file mode 100755 index 000000000..fb6dc0742 --- /dev/null +++ b/http-tests/admin/packages/install-package.sh @@ -0,0 +1,36 @@ +#!/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" + +# test package URI (SKOS package) +package_uri="https://packages.linkeddatahub.com/skos/#this" + +# install package via POST to packages/install endpoint +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=$package_uri" \ + "$ADMIN_BASE_URL"packages/install \ +| grep -q "$STATUS_SEE_OTHER" + +# verify package stylesheet was installed to filesystem +# path should be: /static/com/linkeddatahub/packages/skos/layout.xsl +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + test -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl + +# verify master stylesheet was regenerated and includes package import +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + grep -q "com/linkeddatahub/packages/skos/layout.xsl" \ + /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl diff --git a/http-tests/admin/packages/install-uninstall-package.sh b/http-tests/admin/packages/install-uninstall-package.sh new file mode 100755 index 000000000..4d0b48625 --- /dev/null +++ b/http-tests/admin/packages/install-uninstall-package.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# test package URI (SKOS package) +package_uri="https://packages.linkeddatahub.com/skos/#this" + +# ensure package is not installed initially +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + test ! -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl || true + +# install package +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=$package_uri" \ + "$ADMIN_BASE_URL"packages/install \ +| grep -q "$STATUS_SEE_OTHER" + +# verify package was installed +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + test -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl + +# verify master stylesheet includes package +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + grep -q "com/linkeddatahub/packages/skos/layout.xsl" \ + /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl + +# uninstall package +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=$package_uri" \ + "$ADMIN_BASE_URL"packages/uninstall \ +| grep -q "$STATUS_SEE_OTHER" + +# verify package was uninstalled +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + test ! -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl + +# verify master stylesheet no longer includes package +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + bash -c '! grep -q "com/linkeddatahub/packages/skos/layout.xsl" /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl' diff --git a/http-tests/admin/packages/uninstall-package-400.sh b/http-tests/admin/packages/uninstall-package-400.sh new file mode 100755 index 000000000..4c341dd58 --- /dev/null +++ b/http-tests/admin/packages/uninstall-package-400.sh @@ -0,0 +1,16 @@ +#!/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" + +# Missing package-uri parameter should return 400 Bad Request +curl -k -w "%{http_code}\n" -o /dev/null -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + "$ADMIN_BASE_URL"packages/uninstall \ +| grep -q "$STATUS_BAD_REQUEST" diff --git a/http-tests/admin/packages/uninstall-package.sh b/http-tests/admin/packages/uninstall-package.sh new file mode 100755 index 000000000..4a1042e89 --- /dev/null +++ b/http-tests/admin/packages/uninstall-package.sh @@ -0,0 +1,50 @@ +#!/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" + +# test package URI (SKOS package) +package_uri="https://packages.linkeddatahub.com/skos/#this" + +# first install the package +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=$package_uri" \ + "$ADMIN_BASE_URL"packages/install \ +| grep -q "$STATUS_SEE_OTHER" + +# verify package stylesheet exists before uninstall +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + test -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl + +# uninstall package via POST to packages/uninstall endpoint +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "package-uri=$package_uri" \ + "$ADMIN_BASE_URL"packages/uninstall \ +| grep -q "$STATUS_SEE_OTHER" + +# verify package stylesheet was deleted +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + test ! -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl + +# verify master stylesheet was regenerated without package import +docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ + -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ + --env-file "$HTTP_TEST_ROOT/.env" \ + exec -T linkeddatahub \ + bash -c '! grep -q "com/linkeddatahub/packages/skos/layout.xsl" /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl' diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java index 77ab794a5..6539fed8a 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java @@ -28,7 +28,9 @@ import jakarta.ws.rs.FormParam; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Context; @@ -153,7 +155,8 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam(" * * @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this) * @return Package instance - * @throws InternalServerErrorException if package cannot be loaded + * @throws NotFoundException if package cannot be found (404) + * @throws InternalServerErrorException if package cannot be loaded for other reasons */ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String packageURI) { @@ -166,6 +169,12 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package return model.getResource(packageURI).as(com.atomgraph.linkeddatahub.apps.model.Package.class); } + catch (WebApplicationException e) + { + // Re-throw HTTP client errors from LinkedDataClient as-is (404, 403, etc.) + log.error("HTTP error loading package from: {}", packageURI, e); + throw e; + } catch (Exception e) { log.error("Failed to load package from: {}", packageURI, e); From 3e58d875d972adaba73b0b091436d25d3d3b044e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= <martynas@atomgraph.com> Date: Mon, 1 Dec 2025 15:51:38 +0100 Subject: [PATCH 12/12] Package implementation and test fixes --- .../admin/packages/install-package-400.sh | 2 +- ...-package-401.sh => install-package-403.sh} | 6 +- .../admin/packages/install-package-404.sh | 2 +- http-tests/admin/packages/install-package.sh | 20 +-- .../packages/install-uninstall-package.sh | 41 ++---- .../admin/packages/uninstall-package-400.sh | 2 +- .../admin/packages/uninstall-package.sh | 30 ++--- .../resource/admin/pkg/Install.java | 61 +++++++-- .../linkeddatahub/packages/skos/layout.xsl | 61 +++++++++ .../com/linkeddatahub/packages/skos/ns.ttl | 123 ++++++++++++++++++ .../linkeddatahub/packages/skos/package.ttl | 20 +++ src/main/resources/location-mapping.ttl | 4 +- src/main/resources/prefix-mapping.ttl | 4 +- 13 files changed, 293 insertions(+), 83 deletions(-) rename http-tests/admin/packages/{install-package-401.sh => install-package-403.sh} (78%) create mode 100644 src/main/resources/com/linkeddatahub/packages/skos/layout.xsl create mode 100644 src/main/resources/com/linkeddatahub/packages/skos/ns.ttl create mode 100644 src/main/resources/com/linkeddatahub/packages/skos/package.ttl diff --git a/http-tests/admin/packages/install-package-400.sh b/http-tests/admin/packages/install-package-400.sh index ddae0068a..d77736d15 100755 --- a/http-tests/admin/packages/install-package-400.sh +++ b/http-tests/admin/packages/install-package-400.sh @@ -12,5 +12,5 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ - "$ADMIN_BASE_URL"packages/install \ + "${ADMIN_BASE_URL}packages/install" \ | grep -q "$STATUS_BAD_REQUEST" diff --git a/http-tests/admin/packages/install-package-401.sh b/http-tests/admin/packages/install-package-403.sh similarity index 78% rename from http-tests/admin/packages/install-package-401.sh rename to http-tests/admin/packages/install-package-403.sh index c888a50ad..6cba48572 100755 --- a/http-tests/admin/packages/install-package-401.sh +++ b/http-tests/admin/packages/install-package-403.sh @@ -7,10 +7,10 @@ purge_cache "$END_USER_VARNISH_SERVICE" purge_cache "$ADMIN_VARNISH_SERVICE" purge_cache "$FRONTEND_VARNISH_SERVICE" -# Unauthorized access (without certificate) should return 401 Unauthorized +# Unauthorized access (without certificate) should return 403 Forbidden curl -k -w "%{http_code}\n" -o /dev/null -s \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "package-uri=https://packages.linkeddatahub.com/skos/#this" \ - "$ADMIN_BASE_URL"packages/install \ -| grep -q "$STATUS_UNAUTHORIZED" + "${ADMIN_BASE_URL}packages/install" \ +| grep -q "$STATUS_FORBIDDEN" diff --git a/http-tests/admin/packages/install-package-404.sh b/http-tests/admin/packages/install-package-404.sh index 47091ef5d..aafcb92b3 100755 --- a/http-tests/admin/packages/install-package-404.sh +++ b/http-tests/admin/packages/install-package-404.sh @@ -14,5 +14,5 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "package-uri=https://packages.linkeddatahub.com/nonexistent/#package" \ - "$ADMIN_BASE_URL"packages/install \ + "${ADMIN_BASE_URL}packages/install" \ | grep -q "$STATUS_NOT_FOUND" diff --git a/http-tests/admin/packages/install-package.sh b/http-tests/admin/packages/install-package.sh index fb6dc0742..83842c4bf 100755 --- a/http-tests/admin/packages/install-package.sh +++ b/http-tests/admin/packages/install-package.sh @@ -16,21 +16,13 @@ curl -k -w "%{http_code}\n" -o /dev/null -f -s \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "package-uri=$package_uri" \ - "$ADMIN_BASE_URL"packages/install \ + "${ADMIN_BASE_URL}packages/install" \ | grep -q "$STATUS_SEE_OTHER" -# verify package stylesheet was installed to filesystem -# path should be: /static/com/linkeddatahub/packages/skos/layout.xsl -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - test -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl +# verify package stylesheet was installed (should return 200) +curl -k -f -s -o /dev/null \ + "$END_USER_BASE_URL"static/com/linkeddatahub/packages/skos/layout.xsl # verify master stylesheet was regenerated and includes package import -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - grep -q "com/linkeddatahub/packages/skos/layout.xsl" \ - /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl +curl -k -s "${END_USER_BASE_URL}static/localhost/layout.xsl" \ + | grep -q "com/linkeddatahub/packages/skos/layout.xsl" diff --git a/http-tests/admin/packages/install-uninstall-package.sh b/http-tests/admin/packages/install-uninstall-package.sh index 4d0b48625..88a055130 100755 --- a/http-tests/admin/packages/install-uninstall-package.sh +++ b/http-tests/admin/packages/install-uninstall-package.sh @@ -10,13 +10,6 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # test package URI (SKOS package) package_uri="https://packages.linkeddatahub.com/skos/#this" -# ensure package is not installed initially -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - test ! -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl || true - # install package curl -k -w "%{http_code}\n" -o /dev/null -f -s \ -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ @@ -26,20 +19,13 @@ curl -k -w "%{http_code}\n" -o /dev/null -f -s \ "$ADMIN_BASE_URL"packages/install \ | grep -q "$STATUS_SEE_OTHER" -# verify package was installed -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - test -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl +# verify package stylesheet was installed (should return 200) +curl -k -f -s -o /dev/null \ + "${END_USER_BASE_URL}static/com/linkeddatahub/packages/skos/layout.xsl" # verify master stylesheet includes package -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - grep -q "com/linkeddatahub/packages/skos/layout.xsl" \ - /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl +curl -k -s "$END_USER_BASE_URL"static/localhost/layout.xsl \ + | grep -q "com/linkeddatahub/packages/skos/layout.xsl" # uninstall package curl -k -w "%{http_code}\n" -o /dev/null -f -s \ @@ -50,16 +36,11 @@ curl -k -w "%{http_code}\n" -o /dev/null -f -s \ "$ADMIN_BASE_URL"packages/uninstall \ | grep -q "$STATUS_SEE_OTHER" -# verify package was uninstalled -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - test ! -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl +# verify package stylesheet was deleted (should return 404) +curl -k -w "%{http_code}\n" -o /dev/null -s \ + "${END_USER_BASE_URL}static/com/linkeddatahub/packages/skos/layout.xsl" \ +| grep -q "$STATUS_NOT_FOUND" # verify master stylesheet no longer includes package -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - bash -c '! grep -q "com/linkeddatahub/packages/skos/layout.xsl" /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl' +curl -k -s "$END_USER_BASE_URL"static/localhost/layout.xsl \ + | grep -v -q "com/linkeddatahub/packages/skos/layout.xsl" diff --git a/http-tests/admin/packages/uninstall-package-400.sh b/http-tests/admin/packages/uninstall-package-400.sh index 4c341dd58..98a861c60 100755 --- a/http-tests/admin/packages/uninstall-package-400.sh +++ b/http-tests/admin/packages/uninstall-package-400.sh @@ -12,5 +12,5 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ - "$ADMIN_BASE_URL"packages/uninstall \ + "{$ADMIN_BASE_URL}packages/uninstall" \ | grep -q "$STATUS_BAD_REQUEST" diff --git a/http-tests/admin/packages/uninstall-package.sh b/http-tests/admin/packages/uninstall-package.sh index 4a1042e89..027b11ef3 100755 --- a/http-tests/admin/packages/uninstall-package.sh +++ b/http-tests/admin/packages/uninstall-package.sh @@ -16,15 +16,12 @@ curl -k -w "%{http_code}\n" -o /dev/null -f -s \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "package-uri=$package_uri" \ - "$ADMIN_BASE_URL"packages/install \ + "${ADMIN_BASE_URL}packages/install" \ | grep -q "$STATUS_SEE_OTHER" -# verify package stylesheet exists before uninstall -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - test -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl +# verify package stylesheet exists before uninstall (should return 200) +curl -k -f -s -o /dev/null \ + "${END_USER_BASE_URL}static/com/linkeddatahub/packages/skos/layout.xsl" # uninstall package via POST to packages/uninstall endpoint curl -k -w "%{http_code}\n" -o /dev/null -f -s \ @@ -32,19 +29,14 @@ curl -k -w "%{http_code}\n" -o /dev/null -f -s \ -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "package-uri=$package_uri" \ - "$ADMIN_BASE_URL"packages/uninstall \ + "${ADMIN_BASE_URL}packages/uninstall" \ | grep -q "$STATUS_SEE_OTHER" -# verify package stylesheet was deleted -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - test ! -f /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos/layout.xsl +# verify package stylesheet was deleted (should return 404) +curl -k -w "%{http_code}\n" -o /dev/null -s \ + "${END_USER_BASE_URL}static/com/linkeddatahub/packages/skos/layout.xsl" \ +| grep -q "$STATUS_NOT_FOUND" # verify master stylesheet was regenerated without package import -docker compose -f "$HTTP_TEST_ROOT/../docker-compose.yml" \ - -f "$HTTP_TEST_ROOT/docker-compose.http-tests.yml" \ - --env-file "$HTTP_TEST_ROOT/.env" \ - exec -T linkeddatahub \ - bash -c '! grep -q "com/linkeddatahub/packages/skos/layout.xsl" /usr/local/tomcat/webapps/ROOT/static/localhost/layout.xsl' +curl -k -s "$END_USER_BASE_URL"static/localhost/layout.xsl \ + | grep -v -q "com/linkeddatahub/packages/skos/layout.xsl" diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java index 6539fed8a..02c8ab01b 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java @@ -16,6 +16,7 @@ */ 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.LinkedDataClient; @@ -49,6 +50,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.apache.jena.util.FileManager; /** * JAX-RS resource that installs a LinkedDataHub package. @@ -67,6 +69,7 @@ public class Install private final com.atomgraph.linkeddatahub.apps.model.Application application; private final com.atomgraph.linkeddatahub.Application system; + private final DataManager dataManager; @Context ServletContext servletContext; @@ -75,13 +78,16 @@ public class Install * * @param application matched application (admin app) * @param system system application + * @param dataManager data manager */ @Inject public Install(com.atomgraph.linkeddatahub.apps.model.Application application, - com.atomgraph.linkeddatahub.Application system) + com.atomgraph.linkeddatahub.Application system, + DataManager dataManager) { this.application = application; this.system = system; + this.dataManager = dataManager; } /** @@ -112,14 +118,13 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam(" if (ontology == null) throw new BadRequestException("Package ontology not found"); - URI ontologyURI = URI.create(ontology.getURI()); URI stylesheetURI = (stylesheet != null) ? URI.create(stylesheet.getURI()) : null; String packagePath = UriPath.convert(packageURI); // 2. Download and install ontology - if (log.isDebugEnabled()) log.debug("Downloading package ontology from: {}", ontologyURI); - Model ontologyModel = downloadOntology(ontologyURI); + if (log.isDebugEnabled()) log.debug("Downloading package ontology from: {}", ontology.getURI()); + Model ontologyModel = downloadOntology(ontology.getURI()); installOntology(endUserApp, ontologyModel); // 3. Download and install stylesheet if present @@ -164,8 +169,21 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package { if (log.isDebugEnabled()) log.debug("Loading package from: {}", packageURI); - LinkedDataClient ldc = LinkedDataClient.create(getSystem().getClient(), getSystem().getMediaTypes()); - Model model = ldc.getModel(packageURI); + 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 (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, ((FileManager)getDataManager()).hasCachedModel(packageURI)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getDataManager().isMapped(packageURI)); + model = getDataManager().loadModel(packageURI); + } + else + { + LinkedDataClient ldc = LinkedDataClient.create(getSystem().getClient(), getSystem().getMediaTypes()); + model = ldc.getModel(packageURI); + } return model.getResource(packageURI).as(com.atomgraph.linkeddatahub.apps.model.Package.class); } @@ -185,12 +203,23 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package /** * Downloads RDF from a URI using LinkedDataClient. */ - private Model downloadOntology(URI uri) throws IOException + private Model downloadOntology(String uri) throws IOException { - if (log.isDebugEnabled()) log.debug("Downloading RDF from: {}", uri); + if (log.isDebugEnabled()) log.debug("Downloading ontology from: {}", uri); - LinkedDataClient ldc = LinkedDataClient.create(getSystem().getClient(), getSystem().getMediaTypes()); - return ldc.getModel(uri.toString()); + // 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 (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", uri, ((FileManager)getDataManager()).hasCachedModel(uri)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", uri, getDataManager().isMapped(uri)); + return getDataManager().loadModel(uri); + } + else + { + LinkedDataClient ldc = LinkedDataClient.create(getSystem().getClient(), getSystem().getMediaTypes()); + return ldc.getModel(uri); + } } /** @@ -206,9 +235,7 @@ private String downloadStylesheet(URI uri) throws IOException try (Response response = target.request("text/xsl", "text/*;q=0.8").get()) { if (!response.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)) - { throw new IOException("Failed to download XSLT from " + uri + ": " + response.getStatus()); - } return response.readEntity(String.class); } @@ -320,4 +347,14 @@ public ServletContext getServletContext() return servletContext; } + /** + * Returns RDF data manager. + * + * @return RDF data manager + */ + public DataManager getDataManager() + { + return dataManager; + } + } diff --git a/src/main/resources/com/linkeddatahub/packages/skos/layout.xsl b/src/main/resources/com/linkeddatahub/packages/skos/layout.xsl new file mode 100644 index 000000000..bea9ee244 --- /dev/null +++ b/src/main/resources/com/linkeddatahub/packages/skos/layout.xsl @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE xsl:stylesheet [ + <!ENTITY ldh "https://w3id.org/atomgraph/linkeddatahub#"> + <!ENTITY ac "https://w3id.org/atomgraph/client#"> + <!ENTITY rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <!ENTITY rdfs "http://www.w3.org/2000/01/rdf-schema#"> + <!ENTITY xsd "http://www.w3.org/2001/XMLSchema#"> + <!ENTITY skos "http://www.w3.org/2004/02/skos/core#"> + <!ENTITY srx "http://www.w3.org/2005/sparql-results#"> + <!ENTITY foaf "http://xmlns.com/foaf/0.1/"> +]> +<xsl:stylesheet version="2.0" +xmlns="http://www.w3.org/1999/xhtml" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform" +xmlns:xhtml="http://www.w3.org/1999/xhtml" +xmlns:xs="http://www.w3.org/2001/XMLSchema" +xmlns:ldh="&ldh;" +xmlns:ac="∾" +xmlns:rdf="&rdf;" +xmlns:rdfs="&rdfs;" +xmlns:xsd="&xsd;" +xmlns:skos="&skos;" +xmlns:srx="&srx;" +xmlns:foaf="&foaf;" +xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" +exclude-result-prefixes="#all"> + + <xsl:param name="ldh:base" as="xs:anyURI" static="yes"/> + + <xsl:import _href="{resolve-uri('static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl', $ldh:base)}"/> + + <xsl:param name="foaf:Agent" as="document-node()?"/> + + <xsl:template match="rdf:RDF | srx:sparql" mode="xhtml:Style"> + <xsl:param name="load-wymeditor" select="exists($foaf:Agent//@rdf:about)" as="xs:boolean"/> + <xsl:param name="load-yasqe" select="true()" as="xs:boolean"/> + + <xsl:apply-imports/> + + <!-- inject custom Bootstrap theme that overrides the default one --> + <link href="{resolve-uri('static/com/linkeddatahub/demo/skos/css/bootstrap.css', $ac:contextUri)}" rel="stylesheet" type="text/css"/> + <!-- re-apply LinkedDataHub's Bootstrap customizations --> + <link href="{resolve-uri('static/com/atomgraph/linkeddatahub/css/bootstrap.css', $ac:contextUri)}" rel="stylesheet" type="text/css"/> + + <xsl:if test="$load-wymeditor"> + <link href="{resolve-uri('static/com/atomgraph/linkeddatahub/js/wymeditor/skins/default/skin.css', $ac:contextUri)}" rel="stylesheet" type="text/css"/> + </xsl:if> + <xsl:if test="$load-yasqe"> + <link href="{resolve-uri('static/css/yasqe.css', $ac:contextUri)}" rel="stylesheet" type="text/css"/> + </xsl:if> + </xsl:template> + + <xsl:template match="rdf:RDF" mode="xhtml:Style"> + <xsl:next-match/> + + <link href="{resolve-uri('static/com/linkeddatahub/demo/skos/css/bootstrap.css', $ac:contextUri)}" rel="stylesheet" type="text/css"/> + </xsl:template> + + <xsl:template match="skos:narrower | skos:broader | skos:related | skos:member" mode="bs2:PropertyList"/> + +</xsl:stylesheet> \ No newline at end of file diff --git a/src/main/resources/com/linkeddatahub/packages/skos/ns.ttl b/src/main/resources/com/linkeddatahub/packages/skos/ns.ttl new file mode 100644 index 000000000..60bf73711 --- /dev/null +++ b/src/main/resources/com/linkeddatahub/packages/skos/ns.ttl @@ -0,0 +1,123 @@ +@prefix : <#> . +@prefix ns: <ns#> . +@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix sp: <http://spinrdf.org/sp#> . +@prefix spin: <http://spinrdf.org/spin#> . +@prefix dct: <http://purl.org/dc/terms/> . +@prefix skos: <http://www.w3.org/2004/02/skos/core#> . + +# Concept + +# narrower + +skos:Concept ldh:template ns:NarrowerConcepts. + +ns:NarrowerConcepts a ldh:View ; + dct:title "Narrower concepts" ; + spin:query ns:SelectNarrowerConcepts ; + rdfs:isDefinedBy ns: . + +ns:SelectNarrowerConcepts a sp:Select ; + rdfs:label "Select narrower concepts" ; + sp:text """ +PREFIX skos: <http://www.w3.org/2004/02/skos/core#> + +SELECT DISTINCT ?narrower +WHERE + { GRAPH ?graph + { $about skos:narrower ?narrower . + GRAPH ?narrowerGraph + { + ?narrower skos:prefLabel ?prefLabel . + FILTER (langMatches(lang(?prefLabel), "en")) + } + } + } +ORDER BY ?prefLabel +""" ; + rdfs:isDefinedBy ns: . + +# broader + +skos:Concept ldh:template ns:BroaderConcepts. + +ns:BroaderConcepts a ldh:View ; + dct:title "Broader concepts" ; + spin:query ns:SelectBroaderConcepts ; + rdfs:isDefinedBy ns: . + +ns:SelectBroaderConcepts a sp:Select ; + rdfs:label "Select broader concepts" ; + sp:text """ +PREFIX skos: <http://www.w3.org/2004/02/skos/core#> + +SELECT DISTINCT ?broader +WHERE + { GRAPH ?graph + { $about skos:broader ?broader . + GRAPH ?broaderGraph + { + ?broader skos:prefLabel ?prefLabel . + FILTER (langMatches(lang(?prefLabel), "en")) + } + } + } +ORDER BY ?prefLabel +""" ; + rdfs:isDefinedBy ns: . + +# Collection + +skos:Collection ldh:template ns:CollectionMembers. + +ns:CollectionMembers a ldh:View ; + dct:title "Collection members" ; + spin:query ns:SelectCollectionMembers ; + rdfs:isDefinedBy ns: . + +ns:SelectCollectionMembers a sp:Select ; + rdfs:label "Select collection members" ; + sp:text """ +PREFIX skos: <http://www.w3.org/2004/02/skos/core#> + +SELECT DISTINCT ?member +WHERE + { GRAPH ?graph + { $about skos:member ?member . + GRAPH ?memberGraph + { + ?member skos:prefLabel ?prefLabel . + FILTER (langMatches(lang(?prefLabel), "en")) + } + } + } +ORDER BY ?prefLabel +""" ; + rdfs:isDefinedBy ns: . + +# ConceptScheme + +skos:ConceptScheme ldh:template ns:ConceptsInScheme. + +ns:ConceptsInScheme a ldh:View ; + dct:title "Concepts in scheme" ; + spin:query ns:SelectConceptsInScheme ; + rdfs:isDefinedBy ns: . + +ns:SelectConceptsInScheme a sp:Select ; + rdfs:label "Select concepts in scheme" ; + sp:text """ +PREFIX skos: <http://www.w3.org/2004/02/skos/core#> + +SELECT DISTINCT ?concept +WHERE + { GRAPH ?graph + { ?concept skos:inScheme $about ; + skos:prefLabel ?prefLabel . + FILTER (langMatches(lang(?prefLabel), "en")) + } + } +ORDER BY ?prefLabel +""" ; + rdfs:isDefinedBy ns: . diff --git a/src/main/resources/com/linkeddatahub/packages/skos/package.ttl b/src/main/resources/com/linkeddatahub/packages/skos/package.ttl new file mode 100644 index 000000000..8240eccda --- /dev/null +++ b/src/main/resources/com/linkeddatahub/packages/skos/package.ttl @@ -0,0 +1,20 @@ +@base <https://packages.linkeddatahub.com/skos/> . +@prefix : <#> . +@prefix lapp: <https://w3id.org/atomgraph/linkeddatahub/apps#> . +@prefix ldt: <https://www.w3.org/ns/ldt#> . +@prefix ac: <https://w3id.org/atomgraph/client#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix dct: <http://purl.org/dc/terms/> . +@prefix foaf: <http://xmlns.com/foaf/0.1/> . + +<https://packages.linkeddatahub.com/skos/#this> a lapp:Package ; + rdfs:label "SKOS Package" ; + dct:title "SKOS Package for LinkedDataHub" ; + dct:description "Provides SKOS (Simple Knowledge Organization System) vocabulary support with custom templates for concept hierarchies, schemes, and collections." ; + dct:creator <https://atomgraph.com/#company> ; + ldt:ontology <https://raw.githubusercontent.com/AtomGraph/LinkedDataHub-Apps/refs/heads/develop/packages/skos/ns.ttl> ; + ac:stylesheet <https://raw.githubusercontent.com/AtomGraph/LinkedDataHub-Apps/refs/heads/develop/packages/skos/layout.xsl> . + +<https://atomgraph.com/#company> a foaf:Organization ; + foaf:name "AtomGraph" ; + foaf:homepage <https://atomgraph.com> . diff --git a/src/main/resources/location-mapping.ttl b/src/main/resources/location-mapping.ttl index 6db880201..91856575c 100644 --- a/src/main/resources/location-mapping.ttl +++ b/src/main/resources/location-mapping.ttl @@ -16,5 +16,7 @@ [ lm:name "http://spinrdf.org/spin#" ; lm:altName "etc/spin.ttl" ] , [ lm:name "http://spinrdf.org/spin" ; lm:altName "etc/spin.ttl" ] , [ lm:name "http://spinrdf.org/spl#" ; lm:altName "etc/spl.spin.ttl" ] , - [ lm:name "http://spinrdf.org/spl" ; lm:altName "etc/spl.spin.ttl" ] + [ lm:name "http://spinrdf.org/spl" ; lm:altName "etc/spl.spin.ttl" ] , + [ lm:name "https://packages.linkeddatahub.com/skos/" ; lm:altName "com/linkeddatahub/packages/skos/package.ttl" ] , + [ lm:name "https://raw.githubusercontent.com/AtomGraph/LinkedDataHub-Apps/refs/heads/develop/packages/skos/ns.ttl" ; lm:altName "com/linkeddatahub/packages/skos/ns.ttl" ] . \ No newline at end of file diff --git a/src/main/resources/prefix-mapping.ttl b/src/main/resources/prefix-mapping.ttl index d82a1f554..85e7c9a84 100644 --- a/src/main/resources/prefix-mapping.ttl +++ b/src/main/resources/prefix-mapping.ttl @@ -73,5 +73,7 @@ [ lm:prefix "http://purl.org/goodrelations/v1" ; lm:altName "com/atomgraph/client/goodrelations.owl" ] , [ lm:prefix "http://spinrdf.org/sp" ; lm:altName "etc/sp.ttl" ] , [ lm:prefix "http://spinrdf.org/spin" ; lm:altName "etc/spin.ttl" ] , - [ lm:prefix "http://spinrdf.org/spl" ; lm:altName "etc/spl.spin.ttl" ] + [ lm:prefix "http://spinrdf.org/spl" ; lm:altName "etc/spl.spin.ttl" ] , + [ lm:prefix "https://packages.linkeddatahub.com/skos/" ; lm:altName "com/linkeddatahub/packages/skos/package.ttl" ] , + [ lm:prefix "https://raw.githubusercontent.com/AtomGraph/LinkedDataHub-Apps/refs/heads/develop/packages/skos/ns.ttl" ; lm:altName "com/linkeddatahub/packages/skos/ns.ttl" ] . \ No newline at end of file