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/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/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/admin/packages/install-package-400.sh b/http-tests/admin/packages/install-package-400.sh new file mode 100755 index 000000000..d77736d15 --- /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-403.sh b/http-tests/admin/packages/install-package-403.sh new file mode 100755 index 000000000..6cba48572 --- /dev/null +++ b/http-tests/admin/packages/install-package-403.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 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_FORBIDDEN" 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..aafcb92b3 --- /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..83842c4bf --- /dev/null +++ b/http-tests/admin/packages/install-package.sh @@ -0,0 +1,28 @@ +#!/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 (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 +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 new file mode 100755 index 000000000..88a055130 --- /dev/null +++ b/http-tests/admin/packages/install-uninstall-package.sh @@ -0,0 +1,46 @@ +#!/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 +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 (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 +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 \ + -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 (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 +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 new file mode 100755 index 000000000..98a861c60 --- /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..027b11ef3 --- /dev/null +++ b/http-tests/admin/packages/uninstall-package.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# 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 (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 \ + -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 (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 +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/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/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/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/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 + + +{ + + a foaf:Document ; + dct:title "Install package endpoint" . + +} + + +{ + + a foaf:Document ; + dct:title "Uninstall package endpoint" . + +} + +# CONTAINERS + + +{ + + a dh:Container ; + sioc:has_parent <> ; + dct:title "Packages" ; + dct:description "Manage installed packages" ; + rdf:_1 . + + a ldh:Object ; + rdf:value ldh:ChildrenView . + +} + ### ONTOLOGIES ### # CONTAINERS 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/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 diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index eeebe124a..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); @@ -1309,9 +1310,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/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 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..4ad8b71e5 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Martynas Jusevičius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.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 } + */ +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(); + + /** + * 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 getImportedPackages(); + +} 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..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 @@ -21,15 +21,17 @@ 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; 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; @@ -69,7 +71,7 @@ public URI getBaseURI() @Override public Resource getOrigin() { - return getPropertyResourceValue(LDH.origin); + return getPropertyResourceValue(LAPP.origin); } @Override @@ -118,9 +120,9 @@ public Resource getFrontendProxy() public boolean isReadAllowed() { Statement stmt = getProperty(LAPP.allowRead); - + if (stmt != null) return stmt.getBoolean(); - + return false; } @@ -129,4 +131,26 @@ public UriBuilder getUriBuilder() { return UriBuilder.fromUri(getOriginURI()); } + + @Override + public Set getImportedPackages() + { + Set 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..a431ee262 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java @@ -0,0 +1,88 @@ +/** + * Copyright 2025 Martynas Jusevičius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.apps.model.impl; + +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. + * + * @author Martynas Jusevičius {@literal } + */ +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); + } + + @Override + public Set getImportedPackages() + { + Set 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/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 + * + * 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 } + */ +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..02c8ab01b --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Install.java @@ -0,0 +1,360 @@ +/** + * Copyright 2025 Martynas Jusevičius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.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; +import com.atomgraph.linkeddatahub.server.util.UriPath; +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.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; +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; +import org.apache.jena.util.FileManager; + +/** + * 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 } + */ +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 DataManager dataManager; + + @Context ServletContext servletContext; + + /** + * Constructs endpoint. + * + * @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, + DataManager dataManager) + { + this.application = application; + this.system = system; + this.dataManager = dataManager; + } + + /** + * 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 = 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 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: {}", ontology.getURI()); + Model ontologyModel = downloadOntology(ontology.getURI()); + 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); + } + } + + /** + * 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 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) + { + try + { + if (log.isDebugEnabled()) log.debug("Loading package from: {}", 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); + } + 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); + throw new InternalServerErrorException("Failed to load package from: " + packageURI, e); + } + } + + /** + * Downloads RDF from a URI using LinkedDataClient. + */ + private Model downloadOntology(String uri) throws IOException + { + if (log.isDebugEnabled()) log.debug("Downloading ontology from: {}", uri); + + // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request + if (((FileManager)getDataManager()).hasCachedModel(uri) || + (getDataManager().isResolvingMapped() && getDataManager().isMapped(uri))) // read mapped URIs (such as system ontologies) from a file + { + if (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); + } + } + + /** + * 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//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 packages = app.getImportedPackages(); + List packagePaths = new ArrayList<>(); + + for (Resource pkg : packages) + 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(); + 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 RDF data manager. + * + * @return RDF data manager + */ + public DataManager getDataManager() + { + return dataManager; + } + +} 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..1bc5b169f --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/Uninstall.java @@ -0,0 +1,226 @@ +/** + * Copyright 2025 Martynas Jusevičius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.resource.admin.pkg; + +import com.atomgraph.linkeddatahub.apps.model.AdminApplication; +import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; +import com.atomgraph.linkeddatahub.server.util.UriPath; +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 } + */ +public class Uninstall +{ + private static final Logger log = LoggerFactory.getLogger(Uninstall.class); + + private final com.atomgraph.linkeddatahub.apps.model.Application application; + + @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 = UriPath.convert(packageURI); + + // 1. Remove ontology triples from namespace graph + uninstallOntology(endUserApp, packagePath); + + // 2. Delete stylesheet from /static// + 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// + */ + 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 packages = app.getImportedPackages(); + List packagePaths = new ArrayList<>(); + + for (Resource pkg : packages) + { + String pkgPath = UriPath.convert(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; + } + +} 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/ProxyResourceBase.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java index 420a78a95..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 @@ -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; @@ -208,8 +218,78 @@ public Invocation.Builder getBuilder(WebTarget target) 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(); } /** 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 + * + * 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 } + */ +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); + } + } + +} 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 + * + * 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 } + */ +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//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 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 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 6e0c49a11..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" ); @@ -82,4 +85,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..17414f681 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java @@ -101,15 +101,12 @@ 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" ); /** - * 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/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..819120f9b 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 { }""" ; @@ -163,50 +163,38 @@ rdfs:label "Admin application constructor" ; rdfs:isDefinedBy : . -# CONSTRAINTS - -:StartsWithHTTPS a sp:Construct ; - rdfs:label "ldt:base starts with https://" ; - sp:text """ -PREFIX ldt: -PREFIX spin: -PREFIX rdfs: +# package -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://") ) -}""" ; +: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 : . -:BasePathMatchesRegex a sp:Construct ; - rdfs:label "Base URI path does not match regex" ; +# CONSTRAINTS + +:StartsWithHTTPS a sp:Construct ; + 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 +202,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/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/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 @@ + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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: . +@prefix ldh: . +@prefix rdfs: . +@prefix sp: . +@prefix spin: . +@prefix dct: . +@prefix skos: . + +# 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: + +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: + +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: + +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: + +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 . +@prefix : <#> . +@prefix lapp: . +@prefix ldt: . +@prefix ac: . +@prefix rdfs: . +@prefix dct: . +@prefix foaf: . + + 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 ; + ldt:ontology ; + ac:stylesheet . + + a foaf:Organization ; + foaf:name "AtomGraph" ; + foaf:homepage . 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 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..4a8f2e490 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css @@ -64,11 +64,21 @@ 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; } +.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; } .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; } +.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/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"> - - + + 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..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 @@ -441,83 +441,46 @@ exclude-result-prefixes="#all" - - - - - - + + @@ -744,10 +707,16 @@ exclude-result-prefixes="#all" -

- -

+ +

+ +

+
+ + + +
- - - + + + - + - -
-
    +
    + + +
    - + 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..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 @@ -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> @@ -649,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> @@ -671,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> @@ -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> @@ -869,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 --> @@ -919,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?"/> @@ -1213,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> @@ -1249,10 +1244,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/bootstrap/2.3.2/resource.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/resource.xsl index a4780e334..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,22 +261,22 @@ extension-element-prefixes="ixsl" <xsl:attribute name="class" select="concat($class, ' ', 'btn-agent')"/> </xsl:template> - <xsl:template match="*[@rdf:about = '∾ReadMode']" 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:attribute name="class" select="concat($class, ' ', 'btn-read')"/> - </xsl:template> + <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: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 = '∾GraphMode']" mode="ldh:logo"> - <xsl:param name="class" as="xs:string?"/> - - <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"> @@ -397,16 +397,20 @@ 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"/> + <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'"/> @@ -419,14 +423,18 @@ 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"/> </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 3a615bdfa..44f3a7599 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 <xsl:variable name="href" select="xs:anyURI(substring-before(ixsl:get(ixsl:get(ixsl:window(), 'location'), 'href'), '#'))" as="xs:anyURI"/> <!-- set cookie with id_token --> <ixsl:set-property name="cookie" select="concat('LinkedDataHub.id_token=', $id-token, '; path=/; secure')" object="ixsl:page()"/> - <!-- reload page to render with authenticated user context --> - <xsl:variable name="controller" select="ixsl:abort-controller()"/> - <ixsl:set-property name="saxonController" select="$controller" object="ixsl:get(ixsl:window(), 'LinkedDataHub')"/> - <xsl:variable name="request" select="map{ 'method': 'GET', 'href': $href, 'headers': map{ 'Accept': 'application/xhtml+xml' } }" as="map(*)"/> - <xsl:variable name="context" select=" - map{ - 'request': $request, - 'href': $href, - 'push-state': true() - }" as="map(*)"/> - <ixsl:promise select=" - ixsl:http-request($context('request'), $controller) - => ixsl:then(ldh:rethread-response($context, ?)) - => ixsl:then(ldh:handle-response#1) - => ixsl:then(ldh:xhtml-document-loaded#1) - " on-failure="ldh:promise-failure#1"/> + <!-- do a full page refresh to reload with authenticated context --> + <xsl:sequence select="ixsl:call(ixsl:get(ixsl:window(), 'location'), 'replace', [ $href ])"/> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="ixsl:page()" mode="ldh:HTMLDocumentLoaded"> @@ -524,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> @@ -764,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> @@ -849,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> 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>