diff --git a/Makefile b/Makefile index 9d598f80c..38b9fd026 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: sef drop cert release +.PHONY: sef drop cert release tests # Generate Saxon-JS SEF files for client-side XSLT transformations sef: @@ -10,8 +10,12 @@ drop: # Generate server SSL certificate using the .env config cert: - ./bin/server-cert-gen.sh .env nginx ssl + server-cert-gen.sh .env nginx ssl # Run the full Maven release process (prepare, deploy to Sonatype, merge to master/develop) release: ./release.sh + +# Run HTTP tests using owner and secretary certificates with passwords from secrets/ +tests: + cd http-tests && ./run.sh ../ssl/owner/cert.pem $$(cat ../secrets/owner_cert_password.txt) ../ssl/secretary/cert.pem $$(cat ../secrets/secretary_cert_password.txt) diff --git a/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh b/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh new file mode 100755 index 000000000..2ea264ba8 --- /dev/null +++ b/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh @@ -0,0 +1,40 @@ +#!/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" + +# create class in the namespace ontology + +namespace_doc="${END_USER_BASE_URL}ns" +namespace="${namespace_doc}#" +ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" +class="${namespace}NewClass" + +add-class.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + -b "$ADMIN_BASE_URL" \ + --uri "$class" \ + --label "New class" \ + "$ontology_doc" + +# clear ontology from memory + +clear-ontology.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + -b "$ADMIN_BASE_URL" \ + --ontology "$namespace" + +# query using relative URI - <#NewClass> should resolve to ${namespace}NewClass + +curl -k -s -G \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: application/sparql-results+xml" \ + "${namespace_doc}" \ + --data-urlencode "query=SELECT * { <#NewClass> ?p ?o }" \ +| grep 'New class' > /dev/null diff --git a/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh b/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh new file mode 100644 index 000000000..a0654c887 --- /dev/null +++ b/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh @@ -0,0 +1,41 @@ +#!/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" + +# create class in the namespace ontology + +namespace_doc="${END_USER_BASE_URL}ns" +namespace="${namespace_doc}#" +ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" +class="${namespace}NewClass" + +add-class.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + -b "$ADMIN_BASE_URL" \ + --uri "$class" \ + --label "New class" \ + "$ontology_doc" + +# clear ontology from memory + +clear-ontology.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + -b "$ADMIN_BASE_URL" \ + --ontology "$namespace" + +# query using relative URI - <#NewClass> should resolve to ${namespace}NewClass + +curl -k -s -X POST \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: application/sparql-results+xml" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + "${namespace_doc}" \ + --data-urlencode "query=SELECT * { <#NewClass> ?p ?o }" \ +| grep 'New class' > /dev/null diff --git a/pom.xml b/pom.xml index 781efb2ed..2586a6b1a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.3.0 + 5.3.1-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub @@ -46,7 +46,7 @@ https://github.com/AtomGraph/LinkedDataHub scm:git:git://github.com/AtomGraph/LinkedDataHub.git scm:git:git@github.com:AtomGraph/LinkedDataHub.git - linkeddatahub-5.3.0 + linkeddatahub-2.1.1 diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 918b017fb..6e8991f1c 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -70,7 +70,8 @@ import com.atomgraph.client.util.XsltResolver; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.client.filter.ClientUriRewriteFilter; -import com.atomgraph.linkeddatahub.client.filter.grddl.YouTubeGRDDLFilter; +import com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilter; +import com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilterProvider; import com.atomgraph.linkeddatahub.imports.ImportExecutor; import com.atomgraph.linkeddatahub.io.HtmlJsonLDReaderFactory; import com.atomgraph.linkeddatahub.io.JsonLDReader; @@ -153,6 +154,7 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; +import java.util.ServiceLoader; import java.util.Map; import java.util.Properties; import jakarta.mail.Authenticator; @@ -1168,17 +1170,19 @@ protected void registerExceptionMappers() */ protected void registerClientFilters() { - try - { - // Register YouTube GRDDL filter - YouTubeGRDDLFilter youtubeFilter = new YouTubeGRDDLFilter(xsltComp); - client.register(youtubeFilter); - externalClient.register(youtubeFilter); - } - catch (SaxonApiException ex) + ServiceLoader.load(JSONGRDDLFilterProvider.class).forEach(provider -> { - if (log.isErrorEnabled()) log.error("Failed to initialize GRDDL client filter"); - } + try + { + JSONGRDDLFilter filter = provider.getFilter(xsltComp); + client.register(filter); + externalClient.register(filter); + } + catch (SaxonApiException ex) + { + if (log.isErrorEnabled()) log.error("Failed to initialize GRDDL client filter for {}", provider.getClass().getSimpleName()); + } + }); } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/client/filter/JSONGRDDLFilterProvider.java b/src/main/java/com/atomgraph/linkeddatahub/client/filter/JSONGRDDLFilterProvider.java new file mode 100644 index 000000000..9fa163c16 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/client/filter/JSONGRDDLFilterProvider.java @@ -0,0 +1,31 @@ +/** + * 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.client.filter; + +import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.XsltCompiler; + +/** + * SPI interface for providing {@link JSONGRDDLFilter} instances. + * Implementations are discovered via {@link java.util.ServiceLoader}. + */ +public interface JSONGRDDLFilterProvider +{ + + JSONGRDDLFilter getFilter(XsltCompiler xsltCompiler) throws SaxonApiException; + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/client/filter/grddl/YouTubeGRDDLFilterProvider.java b/src/main/java/com/atomgraph/linkeddatahub/client/filter/grddl/YouTubeGRDDLFilterProvider.java new file mode 100644 index 000000000..3bcb37a95 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/client/filter/grddl/YouTubeGRDDLFilterProvider.java @@ -0,0 +1,36 @@ +/** + * 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.client.filter.grddl; + +import com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilter; +import com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilterProvider; +import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.XsltCompiler; + +/** + * {@link JSONGRDDLFilterProvider} implementation for YouTube GRDDL transformation. + */ +public class YouTubeGRDDLFilterProvider implements JSONGRDDLFilterProvider +{ + + @Override + public JSONGRDDLFilter getFilter(XsltCompiler xsltCompiler) throws SaxonApiException + { + return new YouTubeGRDDLFilter(xsltCompiler); + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java index 8c234be2a..095219cf3 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java @@ -51,6 +51,7 @@ import org.apache.jena.ontology.Ontology; import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.riot.system.Checker; @@ -148,6 +149,8 @@ public Response get(@QueryParam(QUERY) Query query, else throw new BadRequestException("SPARQL query string not provided"); } + // re-parse query to set explicit fallback base URI + query = QueryFactory.create(query.toString(), getUriInfo().getAbsolutePath().toString()); return super.get(query, defaultGraphUris, namedGraphUris); } @@ -160,6 +163,8 @@ public Response post(@FormParam(QUERY) String queryString, @FormParam(UPDATE) St { if (updateString != null) throw new WebApplicationException("SPARQL updates are not allowed on the endpoint", Status.METHOD_NOT_ALLOWED); + // re-parse query to set explicit fallback base URI + queryString = QueryFactory.create(queryString, getUriInfo().getAbsolutePath().toString()).toString(); return super.post(queryString, updateString, defaultGraphUris, namedGraphUris, usingGraphUris, usingNamedGraphUris); } diff --git a/src/main/resources/META-INF/services/com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilterProvider b/src/main/resources/META-INF/services/com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilterProvider new file mode 100644 index 000000000..d81a47a3f --- /dev/null +++ b/src/main/resources/META-INF/services/com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilterProvider @@ -0,0 +1 @@ +com.atomgraph.linkeddatahub.client.filter.grddl.YouTubeGRDDLFilterProvider 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 9ed7ae6e6..4bdd0c247 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,6 @@ exclude-result-prefixes="#all"> -