From 346625bafc55853a6d7f36d6861d776ffca4307e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 4 Jun 2026 22:23:34 +0200 Subject: [PATCH 01/32] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 22c835849..abcad6aaa 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.4.0 + 5.4.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.4.0 + linkeddatahub-2.1.1 From d7e95217819971b593eb586e46621f606d32eebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 5 Jun 2026 11:38:10 +0200 Subject: [PATCH 02/32] Jena 6.1.0 (#309) * Core SNAPSHOT bump Jena version bump to 6.0.0 * POM update * Tomcat base image bump * JUnit bumped to v5 * Add mockito-junit-jupiter dependency Required by ProxyRequestFilterTest after the JUnit 4 -> 5 migration: @ExtendWith(MockitoExtension.class) lives in mockito-junit-jupiter, not mockito-core. * Bump docker-compose image versions - nginx 1.23.3 -> 1.31.1 - atomgraph/fuseki 4.7.0 -> 6.1.0 (matches Jena 6 upgrade) - varnish 7.3.0 -> 7.7.3 * CI: dump container logs on failure * DEBUG: trace entrypoint with set -x (revert before merge) * CI: dump Tomcat logs from linkeddatahub container on failure * Servlet API bump * Jena bump to 6.1.0 * CI: install Java 21 for Jena 6.1.0 CLI tools Jena 6.1.0 binaries are compiled for Java 21; the runner default is Java 17 which produced UnsupportedClassVersionError (class file 65.0 vs 61.0) on every turtle/riot/sparql invocation in the HTTP tests. * Dependency bumps --- .github/workflows/http-tests.yml | 13 ++- Dockerfile | 6 +- docker-compose.yml | 12 +-- platform/entrypoint.sh | 2 + pom.xml | 79 ++++++++++++------- .../client/SesameProtocolClient.java | 2 +- .../linkeddatahub/resource/Namespace.java | 13 +-- .../server/filter/request/OntologyFilter.java | 2 +- .../server/util/OntologyModelGetter.java | 2 +- .../atomgraph/core/io/BillionLaughsTest.java | 37 +++++---- .../filter/ClientUriRewriteFilterTest.java | 4 +- .../request/ProxyRequestFilterTest.java | 12 +-- .../server/util/URLValidatorTest.java | 23 +++--- 13 files changed, 120 insertions(+), 87 deletions(-) diff --git a/.github/workflows/http-tests.yml b/.github/workflows/http-tests.yml index 22e9740bd..adac05737 100644 --- a/.github/workflows/http-tests.yml +++ b/.github/workflows/http-tests.yml @@ -8,11 +8,16 @@ jobs: runs-on: ubuntu-latest env: ASF_ARCHIVE: https://archive.apache.org/dist/ - JENA_VERSION: 4.7.0 + JENA_VERSION: 6.1.0 BASE_URI: https://localhost:4443/ steps: - name: Install Linux packages run: sudo apt-get update && sudo apt-get install -qq raptor2-utils && sudo apt-get install curl + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' - name: Download Jena run: curl -sS --fail "${{ env.ASF_ARCHIVE }}jena/binaries/apache-jena-${{ env.JENA_VERSION }}.tar.gz" -o "${{ runner.temp }}/jena.tar.gz" - name: Unpack Jena @@ -49,6 +54,12 @@ jobs: run: ./run.sh "$PWD/ssl/owner/cert.pem" "${{ secrets.HTTP_TEST_OWNER_CERT_PASSWORD }}" "$PWD/ssl/secretary/cert.pem" "${{ secrets.HTTP_TEST_SECRETARY_CERT_PASSWORD }}" shell: bash working-directory: http-tests + - name: Dump container logs on failure + if: failure() + run: docker compose --env-file ./http-tests/.env logs --no-color + - name: Dump Tomcat logs from linkeddatahub container on failure + if: failure() + run: docker compose --env-file ./http-tests/.env exec -T linkeddatahub sh -c 'for f in /usr/local/tomcat/logs/*; do echo "=== $f ==="; cat "$f"; done' || true - name: Stop Docker containers and remove volumes run: docker compose --env-file ./http-tests/.env down -v - name: Remove Docker containers diff --git a/Dockerfile b/Dockerfile index 3d8066e6e..a5fdd51e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM maven:3.8.4-openjdk-17 AS maven +FROM maven:3.9-eclipse-temurin-21 AS maven # download and extract Jena -ARG JENA_VERSION=4.7.0 +ARG JENA_VERSION=6.1.0 ARG JENA_TAR_URL="https://archive.apache.org/dist/jena/binaries/apache-jena-${JENA_VERSION}.tar.gz" @@ -22,7 +22,7 @@ RUN mvn -Pstandalone clean install # ============================== -FROM atomgraph/letsencrypt-tomcat:10.1.46 +FROM atomgraph/letsencrypt-tomcat:10.1.52 LABEL maintainer="martynas@atomgraph.com" diff --git a/docker-compose.yml b/docker-compose.yml index c95560dbd..02b5f5203 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ volumes: varnish_end_user_cache: services: nginx: - image: nginx:1.23.3 + image: nginx:1.31.1 mem_limit: 128m configs: - source: nginx_conf @@ -105,7 +105,7 @@ services: - ./config/dataspaces.trig:/var/linkeddatahub/datasets/dataspaces.trig - ./config/system.trig:/var/linkeddatahub/datasets/system.trig fuseki-admin: - image: atomgraph/fuseki:4.7.0 + image: atomgraph/fuseki:6.1.0 user: root # otherwise fuseki user does not have permissions to the mounted folder which is owner by root expose: - 3030 @@ -114,7 +114,7 @@ services: - ./fuseki/admin:/fuseki/databases command: [ "--config", "/fuseki/config.ttl" ] fuseki-end-user: - image: atomgraph/fuseki:4.7.0 + image: atomgraph/fuseki:6.1.0 user: root # otherwise the fuseki user does not have permissions to the mounted folder which is owner by root expose: - 3030 @@ -123,7 +123,7 @@ services: - ./fuseki/end-user:/fuseki/databases command: [ "--config", "/fuseki/config.ttl" ] varnish-frontend: - image: varnish:7.3.0 + image: varnish:7.7.3 user: root # otherwise varnish user does not have permissions to the mounted folder which is owner by root configs: - source: varnish-frontend_vcl @@ -136,7 +136,7 @@ services: entrypoint: varnishd command: [ "-F", "-f", "/etc/varnish/default.vcl", "-a", "http=:6060,HTTP", "-a", "proxy=:8443,PROXY", "-p", "feature=+http2", "-s", "file,/var/lib/varnish/storage.bin,3G", "-t", "86400" ] # -F: foreground, -f: config, -a: listeners, -p: http2, -s: storage, -t: TTL varnish-admin: - image: varnish:7.3.0 + image: varnish:7.7.3 user: root # otherwise the varnish user does not have permissions to the mounted folder which is owner by root configs: - source: varnish-admin_vcl @@ -147,7 +147,7 @@ services: entrypoint: varnishd command: [ "-F", "-f", "/etc/varnish/default.vcl", "-a", "http=:80,HTTP", "-a", "proxy=:8443,PROXY", "-p", "feature=+http2", "-s", "malloc,1G", "-t", "86400", "-p", "timeout_idle=60s" ] # -F: foreground, -f: config, -a: listeners, -p: http2 + idle timeout, -s: storage, -t: TTL varnish-end-user: - image: varnish:7.3.0 + image: varnish:7.7.3 user: root # otherwise varnish user does not have permissions to the mounted folder which is owner by root configs: - source: varnish-end-user_vcl diff --git a/platform/entrypoint.sh b/platform/entrypoint.sh index a737c3a6b..a67ef5383 100755 --- a/platform/entrypoint.sh +++ b/platform/entrypoint.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash set -e +set -x # DEBUG: trace every command (revert before merge) +exec 2>&1 # DEBUG: merge stderr into stdout so the trace lands in docker logs # set timezone diff --git a/pom.xml b/pom.xml index abcad6aaa..3294574f1 100644 --- a/pom.xml +++ b/pom.xml @@ -86,28 +86,28 @@ jakarta.servlet jakarta.servlet-api - 5.0.0 + 6.0.0 provided org.glassfish.jersey.connectors jersey-apache-connector - 3.1.0 + 3.1.11 org.glassfish.jersey.media jersey-media-multipart - 3.1.0 + 3.1.11 org.glassfish.jersey.media jersey-media-json-processing - 3.1.0 + 3.1.11 com.sun.mail jakarta.mail - 2.0.1 + 2.0.2 com.google.guava @@ -117,7 +117,7 @@ com.atomgraph.etl.csv csv2rdf - 2.1.11 + 2.2.0 @@ -129,57 +129,67 @@ org.apache.jena jena-shacl - 4.7.0 + 6.1.0 + + + org.apache.jena + jena-arq + 6.1.0 + + + com.github.jsonld-java + jsonld-java + 0.13.4 ${project.groupId} twirl - 1.0.33 + 1.1.0 ${project.groupId} client - 4.2.11 + 4.3.0 classes ${project.groupId} client - 4.2.11 + 4.3.0 war org.apache.httpcomponents httpclient-cache - 4.5.13 + 4.5.14 com.auth0 java-jwt - 3.19.3 + 3.19.4 net.jodah expiringmap - 0.5.10 + 0.5.11 org.jsoup jsoup - 1.15.3 + 1.22.1 org.apache.tomcat tomcat-coyote - 10.1.2 + 10.1.52 jar - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter + 5.11.4 test @@ -188,6 +198,12 @@ 5.12.0 test + + org.mockito + mockito-junit-jupiter + 5.12.0 + test + @@ -216,10 +232,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + org.apache.maven.plugins maven-release-plugin - 2.5.3 + 3.3.1 release @@ -227,10 +248,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.14.1 - 17 - 17 + 21 + 21 -XDignore.symbol.file @@ -243,7 +264,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.9.0 + 0.10.0 true central-portal-snapshots @@ -254,7 +275,7 @@ org.codehaus.mojo exec-maven-plugin - 3.0.0 + 3.6.3 find-xsl-files @@ -287,7 +308,7 @@ org.honton.chas readfiles-maven-plugin - 0.0.1 + 0.1.0 read-comma-separated-xsl-file-list @@ -338,7 +359,7 @@ com.github.eirslett frontend-maven-plugin - 1.15.1 + 1.15.4 v22.16.0 11.4.2 @@ -366,7 +387,7 @@ org.apache.maven.plugins maven-war-plugin - 3.3.2 + 3.5.1 ${build.warName} ${project.build.directory}/${build.warName} @@ -432,7 +453,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.4.0 attach-sources @@ -445,7 +466,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.2.0 + 3.12.0 attach-javadocs diff --git a/src/main/java/com/atomgraph/linkeddatahub/client/SesameProtocolClient.java b/src/main/java/com/atomgraph/linkeddatahub/client/SesameProtocolClient.java index 0cb29e2c2..1adae7818 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/client/SesameProtocolClient.java +++ b/src/main/java/com/atomgraph/linkeddatahub/client/SesameProtocolClient.java @@ -123,7 +123,7 @@ public static MultivaluedMap solutionMapToMultivaluedMap(QuerySo { String varName = it.next(); RDFNode node = qsm.get(varName); - params.add("$" + varName, NodeFmtLib.str(node.asNode())); + params.add("$" + varName, NodeFmtLib.strNT(node.asNode())); } return params; diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java index 095219cf3..4ccefa319 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java @@ -46,15 +46,13 @@ import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.iri.IRI; -import org.apache.jena.iri.IRIFactory; +import org.apache.jena.irix.IRIx; 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; import org.apache.jena.update.UpdateRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -182,15 +180,12 @@ public Response post(UpdateRequest update, @QueryParam(USING_GRAPH_URI) List """; - @Test(expected = RiotException.class) + @Test public void testBillionLaughs() { - Model m = ModelFactory.createDefaultModel(); - - ByteArrayInputStream bais = new ByteArrayInputStream(MALICIOUS.getBytes(StandardCharsets.UTF_8)); - - RDFParser parser = RDFParser.create() - .lang(Lang.RDFXML) - .errorHandler(ErrorHandlerFactory.errorHandlerStrict) - .checking(true) - .base("http://example.org/") - .source(bais) - .build(); - - System.out.println("Starting parse..."); - parser.parse(StreamRDFLib.graph(m.getGraph())); - System.out.println("Model size: " + m.size()); + assertThrows(RiotException.class, () -> { + Model m = ModelFactory.createDefaultModel(); + + ByteArrayInputStream bais = new ByteArrayInputStream(MALICIOUS.getBytes(StandardCharsets.UTF_8)); + + RDFParser parser = RDFParser.create() + .lang(Lang.RDFXML) + .errorHandler(ErrorHandlerFactory.errorHandlerStrict) + .checking(true) + .base("http://example.org/") + .source(bais) + .build(); + + System.out.println("Starting parse..."); + parser.parse(StreamRDFLib.graph(m.getGraph())); + System.out.println("Model size: " + m.size()); + }); } } diff --git a/src/test/java/com/atomgraph/linkeddatahub/client/filter/ClientUriRewriteFilterTest.java b/src/test/java/com/atomgraph/linkeddatahub/client/filter/ClientUriRewriteFilterTest.java index 1c874c19b..ce3d6e89f 100644 --- a/src/test/java/com/atomgraph/linkeddatahub/client/filter/ClientUriRewriteFilterTest.java +++ b/src/test/java/com/atomgraph/linkeddatahub/client/filter/ClientUriRewriteFilterTest.java @@ -34,8 +34,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for {@link ClientUriRewriteFilter}. diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilterTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilterTest.java index 745ecd94d..2321ecf81 100644 --- a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilterTest.java +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilterTest.java @@ -24,11 +24,11 @@ import java.io.IOException; import java.net.URI; import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.*; @@ -37,7 +37,7 @@ * * @author Martynas Jusevičius {@literal } */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class ProxyRequestFilterTest { @@ -47,7 +47,7 @@ public class ProxyRequestFilterTest private ProxyRequestFilter filter; - @Before + @BeforeEach public void setUp() { filter = new ProxyRequestFilter(); diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/util/URLValidatorTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/util/URLValidatorTest.java index fd22dae18..b72bc4b65 100644 --- a/src/test/java/com/atomgraph/linkeddatahub/server/util/URLValidatorTest.java +++ b/src/test/java/com/atomgraph/linkeddatahub/server/util/URLValidatorTest.java @@ -16,9 +16,10 @@ package com.atomgraph.linkeddatahub.server.util; import com.atomgraph.linkeddatahub.server.exception.InternalURLException; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.net.URI; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Unit tests for URLValidator SSRF protection. @@ -32,34 +33,34 @@ public class URLValidatorTest { - @Test(expected = IllegalArgumentException.class) + @Test public void testNullURI() { - new URLValidator(false).validate(null); + assertThrows(IllegalArgumentException.class, () -> new URLValidator(false).validate(null)); } - @Test(expected = InternalURLException.class) + @Test public void testLinkLocalIPv4Blocked() { - new URLValidator(false).validate(URI.create("http://169.254.1.1:8080/test")); + assertThrows(InternalURLException.class, () -> new URLValidator(false).validate(URI.create("http://169.254.1.1:8080/test"))); } - @Test(expected = InternalURLException.class) + @Test public void testPrivateClass10Blocked() { - new URLValidator(false).validate(URI.create("http://10.0.0.1:8080/test")); + assertThrows(InternalURLException.class, () -> new URLValidator(false).validate(URI.create("http://10.0.0.1:8080/test"))); } - @Test(expected = InternalURLException.class) + @Test public void testPrivateClass172Blocked() { - new URLValidator(false).validate(URI.create("http://172.16.0.0:8080/test")); + assertThrows(InternalURLException.class, () -> new URLValidator(false).validate(URI.create("http://172.16.0.0:8080/test"))); } - @Test(expected = InternalURLException.class) + @Test public void testPrivateClass192Blocked() { - new URLValidator(false).validate(URI.create("http://192.168.1.1:8080/test")); + assertThrows(InternalURLException.class, () -> new URLValidator(false).validate(URI.create("http://192.168.1.1:8080/test"))); } @Test From e8de47343cc70e84c3444dd64fed36ff1c519622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 5 Jun 2026 12:25:42 +0200 Subject: [PATCH 03/32] Remove bash trace debug from entrypoint Drops set -x and the stderr-merge that were left in to debug the startup pipeline; they produced very noisy docker logs. Co-Authored-By: Claude Opus 4.7 (1M context) --- platform/entrypoint.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/platform/entrypoint.sh b/platform/entrypoint.sh index a67ef5383..a737c3a6b 100755 --- a/platform/entrypoint.sh +++ b/platform/entrypoint.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash set -e -set -x # DEBUG: trace every command (revert before merge) -exec 2>&1 # DEBUG: merge stderr into stdout so the trace lands in docker logs # set timezone From 1c47f09995a952e47fb7235ee395c111fcf640e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 5 Jun 2026 17:27:12 +0200 Subject: [PATCH 04/32] Move CSR-only helpers, lapp:Application form restrictions, bs2:FormControl booleans - Move ldh:load-constructed-doc / ldh:set-constructed-doc from the shared imports/default.xsl to client/functions.xsl; they use ixsl:updating and are only called from client-side promise chains, so Saxon-HE was warning about the unlicensed extension on every server start. - Add bs2:FormControl templates for xsd:boolean literals and for the constructor's blank-node boolean placeholder (rdf:type xsd:boolean). - Restrict the lapp:Application settings form to dct:title / dct:description; render the remaining app properties + rdf:type as hidden inputs so PATCH preserves them. Suppress the add-property selector for app settings. - Expose the active application URI as LinkedDataHub.application (set from the Link header) and read it back via lapp:application(), replacing the active-pane dataset lookup. - Dedupe constructor-template properties by predicate URI when merging into the resource form so multiple matching spin:constructors don't produce duplicate placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../xsl/bootstrap/2.3.2/imports/default.xsl | 125 ++++++++++++++---- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/imports/default.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/imports/default.xsl index b5adde1cb..ffa98ad49 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/imports/default.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/imports/default.xsl @@ -269,31 +269,6 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1271,7 +1246,105 @@ exclude-result-prefixes="#all" - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 78bef87081dd987263ab4b47260e4ae5b2bb39b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 5 Jun 2026 17:28:55 +0200 Subject: [PATCH 05/32] Move CSR-only helpers, lapp:Application form restrictions (cont.) Follow-up to 1c47f0999 which only captured the imports/default.xsl half of the change. - Add ldh:load-constructed-doc / ldh:set-constructed-doc to client/functions.xsl (the moved-to home for the CSR-only helpers previously living in imports/default.xsl). - Switch lapp:application() from the active-pane dataset lookup to LinkedDataHub.application on the window object, populated in client.xsl from the Link header on RDF document responses. - Restrict the lapp:Application settings form to dct:title / dct:description; render the remaining app properties + rdf:type as hidden inputs so PATCH preserves them; suppress the add-property selector for app settings. - Dedupe constructor-template properties by predicate URI when merging into the resource form so multiple matching spin:constructors don't produce duplicate placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../xsl/bootstrap/2.3.2/client/functions.xsl | 30 +++++++++++++++++-- .../xsl/bootstrap/2.3.2/client/modal.xsl | 26 +++++++++++++++- .../xsl/bootstrap/2.3.2/resource.xsl | 13 ++++++-- .../atomgraph/linkeddatahub/xsl/client.xsl | 3 ++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl index c4158af6c..b6a10f23c 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl @@ -81,8 +81,7 @@ exclude-result-prefixes="#all" - - + @@ -564,7 +563,32 @@ exclude-result-prefixes="#all" => ixsl:then(ldh:rethread-response($context, ?, $response-key)) "/> - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl index 99d13acdc..dfcb94a8f 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl @@ -39,6 +39,7 @@ xmlns:srx="&srx;" xmlns:ldt="&ldt;" xmlns:sd="&sd;" xmlns:sioc="&sioc;" +xmlns:dct="&dct;" xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" extension-element-prefixes="ixsl" exclude-result-prefixes="#all" @@ -1095,7 +1096,30 @@ LIMIT 10 - + + + + + + + + + + + + + + + + + + + + + + + + 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 de0145d55..53ba06be8 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 @@ -1347,8 +1347,17 @@ extension-element-prefixes="ixsl" - - + + + + + + + + + + + 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 f3517e36b..7f64aa183 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -477,6 +477,9 @@ WHERE + + + From 9adb046e7c2d5a26bea0bbea5e03690796c27178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Fri, 5 Jun 2026 18:46:45 +0200 Subject: [PATCH 06/32] Lift bs2:Form $required to a predicate at rdf:RDF level Modal flows build their $context map before the document is fetched, so they couldn't supply the existing per-resource $required xs:boolean. Promote the rdf:RDF bs2:Form template's $required to a function(element()) as xs:boolean predicate that gets evaluated per child and rebound as the resource-level boolean tunnel. Default is an always-false predicate so callers opt in to suppression. ldh:render-form reads $context('required') and tunnels it; btn-edit and btn-app-settings populate it (type-based and always-true respectively); ldh:render-add-modal-form and ldh:modal-form-submit-violation pass the dh:Container/dh:Item rule explicitly. Resource-level template stays xs:boolean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl | 2 ++ .../linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl | 8 ++++++-- .../linkeddatahub/xsl/bootstrap/2.3.2/document.xsl | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl index 8e419e0c7..300ab0355 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl @@ -571,6 +571,7 @@ WHERE + @@ -587,6 +588,7 @@ WHERE + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl index dfcb94a8f..13ff91b6f 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl @@ -772,6 +772,7 @@ LIMIT 10 + @@ -867,7 +868,8 @@ LIMIT 10 'request': $request, 'block': $block, 'about': $about, - 'method': $method + 'method': $method, + 'required': function($r as element()) as xs:boolean { $r/rdf:type/@rdf:resource = ('&dh;Container', '&dh;Item') } }"/> + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl index 1a4ba2476..a05bdb45b 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/document.xsl @@ -1144,6 +1144,8 @@ extension-element-prefixes="ixsl" + + @@ -1208,7 +1210,7 @@ extension-element-prefixes="ixsl" - + From 197ab9cd32f82286d4d63e9bd2cfc1da57b6b33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Sat, 6 Jun 2026 16:33:20 +0200 Subject: [PATCH 07/32] Convert modal-form metadata fetches to async load/set pairs (#310) * Convert modal-form metadata fetches to async load/set pairs The five modal/typeahead form rendering paths each fetched type-metadata, property-metadata, constraints, and object-metadata synchronously via document() inside their terminal render functions, blocking the UI thread on 3-4 sequential round-trips per render. Promote each fetch to an async load/set pair invoked as steps in the existing ixsl:promise chains: - ldh:load-type-metadata / ldh:set-type-metadata (set-type-metadata stripped of its sync work, now just extracts response body) - ldh:load-constraints / ldh:set-constraints (new pair) - Reuse the existing ldh:load-property-metadata and ldh:load-object-metadata in client/block.xsl, extended to accept pre-computed URIs from context (backward-compatible) Each of the five chains (btn-edit, btn-app-settings, add-constructor, typeahead-row-form, row-form-submit-violation) is extended with the new load/set steps before its terminal render function. The render functions are correspondingly stripped of sync work and now just read metadata from context. ldh:modal-form-submit-violation was sync; converted to start a promise chain and terminate in a new ldh:render-modal-form-violation. To uniformly identify the resource being edited across all three modal flows in the submit-violation re-render path, the Container/Item creation modal now also carries @about on its wrapper div (the new resource URL), matching btn-edit and btn-app-settings. The submit handlers read $block/@about directly without a fallback. Co-Authored-By: Claude Opus 4.7 (1M context) * Sync modal @about and slug UI across Container/Item violation re-render Two small follow-ups to the modal-form refactor so the Container/Item creation modal renders identically before and after a constraint-violation re-render: - subject-slug onkeyup handler now also keeps the modal-constructor div's @about attribute in sync with the new URI (alongside the existing 'su' input value and form @action sync), so the submit handler's $block/@about discriminator stays correct after the user edits the slug. - bs2:FormControl slug-style template (imports/default.xsl) now matches Container/Item resources by either @rdf:nodeID (initial SPIN construction: blank node) or @rdf:about (violation response: carries the submitted URI). The same body works for both because $action carries the resource URL in either case; without this the violation re-render fell through to the generic full-URI control. Co-Authored-By: Claude Opus 4.7 (1M context) * Run modal-form metadata load/set pairs in parallel via ixsl:all The async load/set pair conversion in 3ba3dc6e9 made each metadata fetch a self-contained step but left them threaded sequentially. Independent fetches (shapes, constructors, type-metadata, property-metadata, constraints, object-metadata) read disjoint context keys, so they can fan out via ixsl:all and converge through a single merge step. New helper ldh:fire-load-set-parallel#2 in functions.xsl encapsulates the pattern; new ldh:reset-cursor#0 + ixsl:finally replace duplicated end-of-chain cursor cleanup. Refactored sites: - form.xsl:1415 (typeahead-row-form): 21 hops -> 5, two parallel batches around set-typeahead-form-resource as SHACL+SPIN merge barrier - modal.xsl:820 (add-constructor): 18 hops -> 5 - modal.xsl:1103 (app-settings): 21 hops -> 6, type-metadata pair uses an identity load-fn to preserve the GET request already baked by load-edited-resource Wall-clock per flow: ~4-6 sequential round-trips -> ~2. Co-Authored-By: Claude Opus 4.7 (1M context) * Run row/edit metadata load/set pairs in parallel via ixsl:all Mirrors the modal-form refactor from 244366875 across the remaining form chains: btn-edit row-form, row-form constraint-violation re-render, and the two parallel modal chains (btn-edit + btn-app-settings, and modal-form violation re-render). The serial then-chain is replaced with ldh:fire-load-set-parallel over the load/request/response/set tuples; cursor reset moves into ixsl:finally so it runs on both success and failure paths. Adds ldh:fetch-and-load-edited-resource as the shared seed for chains whose first hop is the edited-resource GET; downstream parallel pairs pass an identity load-fn for type-metadata because the seed already baked the GET-style type-metadata-request into context. Co-Authored-By: Claude Opus 4.7 (1M context) * Drop FormPreSubmit @name strip on empty RDF/POST inputs The template removed @name='ol'/'ou'/'ob' from inputs whose value was empty at submit time, but it mutated the live DOM. When a submit failed constraint validation and the form was re-rendered, the @name attributes stayed stripped; a follow-up submit with newly filled values silently dropped those inputs because parse-rdf-post filters by @name. Net effect: after any first-submit validation error, the next submit produced an empty request, the server accepted it, and the row-form success handler crashed in $new-block (required exactly-one, got zero). ldh:parse-rdf-post already filters empty non-'su' values inline via $value-inputs[@name = 'su' or not(ixsl:get(., 'value') = '')], so this template was redundant on top of being destructive. Co-Authored-By: Claude Opus 4.7 (1M context) * Mark admin subdomain link as external The admin subdomain link in the user dropdown opens in a new tab; tag it with class="external" so it picks up the same affordance (icon, styling) as other external links. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../xsl/bootstrap/2.3.2/client/block.xsl | 22 +- .../xsl/bootstrap/2.3.2/client/form.xsl | 310 ++++++++---------- .../xsl/bootstrap/2.3.2/client/functions.xsl | 46 +++ .../xsl/bootstrap/2.3.2/client/modal.xsl | 234 ++++++------- .../xsl/bootstrap/2.3.2/document.xsl | 48 +-- .../xsl/bootstrap/2.3.2/imports/default.xsl | 4 +- .../xsl/bootstrap/2.3.2/layout.xsl | 2 +- 7 files changed, 356 insertions(+), 310 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block.xsl index 8bb025c9a..dd377cc65 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block.xsl @@ -653,12 +653,15 @@ exclude-result-prefixes="#all" - + + if (map:contains($context, 'object-uris')) then $context('object-uris') + else + let $response := $context($response-key) + return if ($response?status = 200 and $response?media-type = 'application/rdf+xml') + then distinct-values($response?body/rdf:RDF/rdf:Description/*/@rdf:resource[not(key('resources', .))]) + else ()"/> @@ -704,11 +707,14 @@ exclude-result-prefixes="#all" - + + if (map:contains($context, 'property-uris')) then $context('property-uris') + else + let $response := $context($response-key) + return if ($response?status = 200 and $response?media-type = 'application/rdf+xml') + then distinct-values($response?body/rdf:RDF/rdf:Description/*/concat(namespace-uri(), local-name())) + else ()"/> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl index 300ab0355..0e1eb5424 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl @@ -185,12 +185,7 @@ WHERE - - - - - - + @@ -262,26 +257,56 @@ WHERE 'block': $block, 'about': $about }"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -306,11 +331,16 @@ WHERE + + 'types': $types, + 'property-uris': distinct-values($resource/*/concat(namespace-uri(), local-name())), + 'object-uris': distinct-values($resource/*/@rdf:resource[not(key('resources', .))]), + 'document': ., + 'action': if (map:contains($context, 'action')) then $context('action') else ldh:href(ac:absolute-path(ldh:base-uri(.)), map{}) + }), map{ 'duplicates': 'use-last' })"/> @@ -331,34 +361,8 @@ WHERE - - - - - - - - - - - - - - - - - - + - @@ -366,23 +370,6 @@ WHERE - - - - - - - - - - - - - - @@ -522,7 +509,7 @@ WHERE - + @@ -555,43 +542,15 @@ WHERE - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -603,7 +562,53 @@ WHERE - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -679,10 +684,12 @@ WHERE + @@ -717,7 +724,7 @@ WHERE - + @@ -944,28 +951,31 @@ WHERE $context, map{ 'body': $body, - 'types': $types + 'types': $types, + 'endpoint': sd:endpoint(), + 'property-uris': distinct-values($body/rdf:RDF/*[not(@rdf:about = $doc-uri)]/*/concat(namespace-uri(), local-name())), + 'object-uris': distinct-values($body/rdf:RDF/*/*/@rdf:resource[not(key('resources', .))]) } ), map{ 'duplicates': 'use-last' })"/> + + ixsl:then(ldh:render-row-form-violation#1) => + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> + All metadata is fetched async by the upstream chain steps; this function just reads from context. --> @@ -975,25 +985,10 @@ WHERE - - - - - - - - - - - - - - - - - - - + + + + @@ -1018,8 +1013,6 @@ WHERE - - @@ -1292,7 +1285,8 @@ WHERE 'constructed-doc': $merged-doc, 'shapes': $shapes, 'resource': $resource, - 'types': $types + 'types': $types, + 'property-uris': distinct-values($resource/*/concat(namespace-uri(), local-name())) }), map{ 'duplicates': 'use-last' })"/> @@ -1307,26 +1301,15 @@ WHERE + + + - - - - - - - - - - - - - - @@ -1354,8 +1337,6 @@ WHERE - - @@ -1403,20 +1384,19 @@ WHERE }"/> + ixsl:then(ldh:render-typeahead-row-form#1) => + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl index b6a10f23c..e55d06da5 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl @@ -589,6 +589,52 @@ exclude-result-prefixes="#all" + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl index 13ff91b6f..2efd4205a 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl @@ -732,6 +732,20 @@ LIMIT 10 Reads context('constructed-doc') as the async-fetched SPIN-construction; the remaining sync document() calls for type-metadata/property-metadata/constraints are scope for a follow-up refactor. --> + + + + + + + + + + @@ -739,26 +753,13 @@ LIMIT 10 + + + - - - - - - - - - - - - - - - - @@ -777,7 +778,7 @@ LIMIT 10 -