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">
-