From 7e4e72eab0f771630db63e1d3802e72662555c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Apr 2026 22:44:00 +0200 Subject: [PATCH 01/31] adds SPARQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/pom.xml | 1 + plugins/seed-xc-sparql/README.md | 6 + plugins/seed-xc-sparql/pom.xml | 104 +++++++++++++++ .../scdh/seed/xc/jena/ParameterConverter.java | 84 ++++++++++++ .../seed/xc/jena/SparqlTransformation.java | 120 ++++++++++++++++++ .../de.ulbms.scdh.seed.xc.api.Transformation | 1 + .../xc/jena/SparqlTransformationTest.java | 13 ++ 7 files changed, 329 insertions(+) create mode 100644 plugins/seed-xc-sparql/README.md create mode 100644 plugins/seed-xc-sparql/pom.xml create mode 100644 plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java create mode 100644 plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java create mode 100644 plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation create mode 100644 plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java diff --git a/plugins/pom.xml b/plugins/pom.xml index 335236b..3efd9b3 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -17,6 +17,7 @@ resource-providers saxon + seed-xc-sparql diff --git a/plugins/seed-xc-sparql/README.md b/plugins/seed-xc-sparql/README.md new file mode 100644 index 0000000..9fffdc0 --- /dev/null +++ b/plugins/seed-xc-sparql/README.md @@ -0,0 +1,6 @@ +# SPARQL Plugin + + +## Parameters + +https://jena.apache.org/documentation/query/parameterized-sparql-strings.html \ No newline at end of file diff --git a/plugins/seed-xc-sparql/pom.xml b/plugins/seed-xc-sparql/pom.xml new file mode 100644 index 0000000..ffa9e61 --- /dev/null +++ b/plugins/seed-xc-sparql/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + de.ulbms.scdh.seed.xc + seed-xc-plugins + ${revision}${changelist} + + + seed-xc-sparql + SEED XC SPARQL + A plugin for transformations build from SPARQL queries + + + 21 + 21 + UTF-8 + 6.0.0 + + + + + ${project.groupId} + seed-xc-api + ${revision}${changelist} + + + ${project.groupId} + seed-resource-providers + ${revision}${changelist} + test + + + org.apache.jena + jena-arq + ${jena.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + io.quarkus + quarkus-junit5 + test + + + + + + + + + io.smallrye + jandex-maven-plugin + ${smallrye-jandex-plugin.version} + + + make-index + + jandex + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.5.0 + + + + + flatten + + flatten + + process-resources + + + + flatten.clean + + clean + + clean + + + + + + + + diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java new file mode 100644 index 0000000..bbf76fc --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java @@ -0,0 +1,84 @@ +package de.ulbms.scdh.seed.xc.jena; + +import de.ulbms.scdh.seed.xc.api.TransformationPreparationException; +import jakarta.enterprise.context.ApplicationScoped; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import org.apache.jena.query.ParameterizedSparqlString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ParameterConverter} class casts strings to Java objects based on a xs-type. + */ +@ApplicationScoped +public class ParameterConverter { + + private static final Logger LOG = LoggerFactory.getLogger(ParameterConverter.class); + + /** + * Sets the variable with name in the supplied query. + * + * @param name - Name of the SPARQL variable + * @param value - Value to be set + * @param type - type information + * @param query - the parametrized query + * @throws TransformationPreparationException - on a cast failure + */ + public void setQueryParameter(String name, String value, String type, ParameterizedSparqlString query) + throws TransformationPreparationException { + switch (type) { + case "xs:anyURI" -> query.setIri(name, value); + case "xs:string" -> query.setLiteral(name, value); + case "xs:integer" -> { + try { + query.setLiteral(name, Integer.parseInt(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to integer", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:long" -> { + try { + query.setLiteral(name, Long.parseLong(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to long", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:float" -> { + try { + query.setLiteral(name, Float.parseFloat(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to float", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:double" -> { + try { + query.setLiteral(name, Double.parseDouble(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to double", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:date" -> { + try { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat(); + calendar.setTime(sdf.parse(value)); + query.setLiteral(name, calendar); + } catch (ParseException e) { + LOG.error("failed to cast '{}' value of parameter {} to calendar", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + default -> { + // defaults to string again + LOG.error("no valid type information for parameter {}: {}. Using string", name, type); + query.setLiteral(name, value); + } + } + } +} diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java new file mode 100644 index 0000000..7834df7 --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -0,0 +1,120 @@ +package de.ulbms.scdh.seed.xc.jena; + +import de.ulbms.scdh.seed.xc.api.*; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.InternalServerErrorException; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.jena.query.*; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SparqlTransformation} is a {@link Transformation} + * plugin for running SPARQL queries. + */ +public class SparqlTransformation implements Transformation { + + private static final Logger LOG = LoggerFactory.getLogger(SparqlTransformation.class); + + public static final String TRANSFORMATION_TYPE = "sparql"; + + TransformationInfo transformationInfo; + + String query; + + @Inject + ParameterConverter parameterConverter; + + @Override + public String getType() { + return SparqlTransformation.TRANSFORMATION_TYPE; + } + + @Override + public void setup(TransformationInfo transformationInfo, File path) throws ConfigurationException { + this.transformationInfo = transformationInfo; + Path configFile = Paths.get(path.toURI()).toAbsolutePath().normalize(); + Path queryFile = configFile.resolve(transformationInfo.getLocation()); + try { + query = Files.readString(queryFile); + } catch (IOException e) { + LOG.error("failed to read SPARQL query: {}", queryFile); + throw new ConfigurationException("failed to read SPARQL query: " + queryFile); + } + } + + @Override + public TransformationInfo getTransformationInfo() { + return transformationInfo; + } + + @Override + public XsltParameterDetails getTransformationParameters() { + return new XsltParameterDetails(); + } + + @Override + public byte[] transform( + RuntimeParameters parameters, + Config config, + String systemId, + InputStream source, + ResourceProvider resourceProvider) + throws TransformationPreparationException { + // make graph + Dataset graph = RDFParser.source(source).toDataset(); + // make query + ParameterizedSparqlString queryTemplate = new ParameterizedSparqlString(this.query); + if (parameters != null) { + for (String key : parameters.getGlobalParameters().keySet()) { + ParameterDescriptor descriptor = + transformationInfo.getParameterDescriptors().get(key); + String value = parameters.getGlobalParameters().get(key); + if (descriptor == null) { + // assume string + queryTemplate.setLiteral(key, value); + } else { + parameterConverter.setQueryParameter(key, value, descriptor.getType(), queryTemplate); + } + } + } + Query query = queryTemplate.asQuery(); + // execute query + QueryExecution qexec = QueryExecutionFactory.create(query, graph); + Model resultModel = qexec.execConstruct(); + qexec.close(); + // write result back to the wire + ByteArrayOutputStream output = new ByteArrayOutputStream(); + RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); + return output.toByteArray(); + } + + @Override + public Uni transformAsync( + RuntimeParameters parameters, + Config config, + String systemId, + Uni source, + ResourceProvider resourceProvider) { + return source.onItem().transform((sourceStream) -> { + try { + return transform(parameters, config, systemId, sourceStream, resourceProvider); + } catch (TransformationPreparationException e) { + throw new InternalServerErrorException(e.getMessage()); + } + }); + } + + @Override + public String getOutputMediaType() { + return ""; + } +} diff --git a/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation new file mode 100644 index 0000000..b002e95 --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation @@ -0,0 +1 @@ +de.ulbms.scdh.seed.xc.jena.SparqlTransformation \ No newline at end of file diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java new file mode 100644 index 0000000..aede1f2 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -0,0 +1,13 @@ +package de.ulbms.scdh.seed.xc.jena; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class SparqlTransformationTest { + + @Test + public void test() { + assertEquals("", ""); + } +} From 99904d8ba661c3fd485dcd277472ec5ca6c2a876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 09:51:17 +0200 Subject: [PATCH 02/31] adds basic tests for SPARQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../seed/xc/jena/SparqlTransformation.java | 64 ++++++++------- .../xc/jena/SparqlTransformationTest.java | 79 ++++++++++++++++++- .../src/test/resources/data/vc-db-1.rdf | 37 +++++++++ .../src/test/resources/rq/qc1.rq | 6 ++ .../src/test/resources/rq/qs1.rq | 4 + 5 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf create mode 100644 plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq create mode 100644 plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index 7834df7..d37ba6e 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -10,9 +10,7 @@ import java.nio.file.Paths; import org.apache.jena.query.*; import org.apache.jena.rdf.model.Model; -import org.apache.jena.riot.RDFDataMgr; -import org.apache.jena.riot.RDFFormat; -import org.apache.jena.riot.RDFParser; +import org.apache.jena.riot.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,33 +66,43 @@ public byte[] transform( String systemId, InputStream source, ResourceProvider resourceProvider) - throws TransformationPreparationException { - // make graph - Dataset graph = RDFParser.source(source).toDataset(); - // make query - ParameterizedSparqlString queryTemplate = new ParameterizedSparqlString(this.query); - if (parameters != null) { - for (String key : parameters.getGlobalParameters().keySet()) { - ParameterDescriptor descriptor = - transformationInfo.getParameterDescriptors().get(key); - String value = parameters.getGlobalParameters().get(key); - if (descriptor == null) { - // assume string - queryTemplate.setLiteral(key, value); - } else { - parameterConverter.setQueryParameter(key, value, descriptor.getType(), queryTemplate); + throws TransformationPreparationException, TransformationException { + try { + // make graph + Lang lang = RDFLanguages.filenameToLang(systemId, Lang.N3); + LOG.info("trying to parse RDF data from {} as format {}", systemId, lang); + Dataset graph = RDFParser.source(source).lang(lang).toDataset(); + // make query + ParameterizedSparqlString queryTemplate = new ParameterizedSparqlString(this.query); + if (parameters != null) { + for (String key : parameters.getGlobalParameters().keySet()) { + ParameterDescriptor descriptor = + transformationInfo.getParameterDescriptors().get(key); + String value = parameters.getGlobalParameters().get(key); + if (descriptor == null) { + // assume string + queryTemplate.setLiteral(key, value); + } else { + parameterConverter.setQueryParameter(key, value, descriptor.getType(), queryTemplate); + } } } + Query query = queryTemplate.asQuery(); + // execute query + QueryExecution qexec = QueryExecutionFactory.create(query, graph); + Model resultModel = qexec.execConstruct(); + qexec.close(); + // write result back to the wire + ByteArrayOutputStream output = new ByteArrayOutputStream(); + RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); + return output.toByteArray(); + } catch (RiotException e) { + LOG.error("failed to read RDF dataset {}", e.getMessage()); + throw new TransformationException(e); + } catch (QueryExecException e) { + LOG.error("failed to execute SPARQL query: {}", e.getMessage()); + throw new TransformationException(e); } - Query query = queryTemplate.asQuery(); - // execute query - QueryExecution qexec = QueryExecutionFactory.create(query, graph); - Model resultModel = qexec.execConstruct(); - qexec.close(); - // write result back to the wire - ByteArrayOutputStream output = new ByteArrayOutputStream(); - RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); - return output.toByteArray(); } @Override @@ -107,7 +115,7 @@ public Uni transformAsync( return source.onItem().transform((sourceStream) -> { try { return transform(parameters, config, systemId, sourceStream, resourceProvider); - } catch (TransformationPreparationException e) { + } catch (TransformationPreparationException | TransformationException e) { throw new InternalServerErrorException(e.getMessage()); } }); diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java index aede1f2..5bc916c 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -2,12 +2,87 @@ import static org.junit.jupiter.api.Assertions.*; +import de.ulbms.scdh.seed.xc.api.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SparqlTransformationTest { + private static final File DATA_DIR = + Paths.get("src", "test", "resources", "data").toFile(); + + private static final File VCDB1 = new File(DATA_DIR, "vc-db-1.rdf"); + + private static final File RQ_DIR = + Paths.get("src", "test", "resources", "rq").toFile(); + + private static final File CONFIG = RQ_DIR; + + private static TransformationInfo QS1; + + private static TransformationInfo QC1; + + private byte[] output; + + private String getOutput() { + return new String(output, StandardCharsets.UTF_8); + } + + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qs1.rq").getAbsolutePath()); + QS1 = info; + } + + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + QC1 = info; + } + + Transformation transformation; + + @BeforeEach + public void setup() { + transformation = new SparqlTransformation(); + } + + @Test + public void testNoSystemIdNonDefaultLang() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QS1, CONFIG); + InputStream in = new FileInputStream(VCDB1); + assertThrows(TransformationException.class, () -> transformation.transform(null, null, null, in, null)); + } + + @Test + public void testSelectQuery() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QS1, CONFIG); + InputStream in = new FileInputStream(VCDB1); + assertThrows( + TransformationException.class, + () -> transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null)); + } + @Test - public void test() { - assertEquals("", ""); + public void testConstructQuery() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1, CONFIG); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + assertTrue(getOutput().startsWith("")); + assertEquals(1, getOutput().lines().count()); } } diff --git a/plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf b/plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf new file mode 100644 index 0000000..b57eb13 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf @@ -0,0 +1,37 @@ + + + + John Smith + + Smith + John + + + + + Becky Smith + + Smith + Rebecca + + + + + Sarah Jones + + Jones + Sarah + + + + + Matt Jones + + + + diff --git a/plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq b/plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq new file mode 100644 index 0000000..6128145 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq @@ -0,0 +1,6 @@ +CONSTRUCT { + ?x "John Smith" . +} +WHERE + { ?x "John Smith" } + diff --git a/plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq b/plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq new file mode 100644 index 0000000..fe5a103 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq @@ -0,0 +1,4 @@ +SELECT ?x +WHERE + { ?x "John Smith" } + From 57eff25befec5ff6aa3d62f45cf3861cc372e43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 13:29:14 +0200 Subject: [PATCH 03/31] adds Serializer bean for choosing working output format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../ulbms/scdh/seed/xc/jena/Serializer.java | 31 +++++++++++++++++++ .../seed/xc/jena/SparqlTransformation.java | 6 +++- .../xc/jena/SparqlTransformationTest.java | 26 +++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java new file mode 100644 index 0000000..c81a790 --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java @@ -0,0 +1,31 @@ +package de.ulbms.scdh.seed.xc.jena; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFLanguages; + +/** + * The {@link Serializer} adds sufficient information for getting an RDF output format that works. + * This includes choosing an encoding variant. + */ +@ApplicationScoped +public class Serializer { + + public static final Lang DEFAULT = Lang.N3; + + public static RDFFormat getFormat(String transformationContentType, String systemId) { + if (transformationContentType != null) { + Lang lang = RDFLanguages.contentTypeToLang(transformationContentType); + if (lang.equals(Lang.NTRIPLES)) { + return new RDFFormat(lang, RDFFormat.UTF8); + } else if (lang.equals(Lang.TTL)) { + return new RDFFormat(lang, RDFFormat.PRETTY); + } else { + return new RDFFormat(lang); + } + } else { + return new RDFFormat(RDFLanguages.filenameToLang(systemId, DEFAULT)); + } + } +} diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index d37ba6e..2118b3f 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -31,6 +31,9 @@ public class SparqlTransformation implements Transformation { @Inject ParameterConverter parameterConverter; + @Inject + Serializer serializer; + @Override public String getType() { return SparqlTransformation.TRANSFORMATION_TYPE; @@ -94,7 +97,8 @@ public byte[] transform( qexec.close(); // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); - RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); + RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId); + RDFDataMgr.write(output, resultModel, format); return output.toByteArray(); } catch (RiotException e) { LOG.error("failed to read RDF dataset {}", e.getMessage()); diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java index 5bc916c..b4c9103 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -28,6 +28,8 @@ class SparqlTransformationTest { private static TransformationInfo QC1; + private static TransformationInfo QC1_TTL; + private byte[] output; private String getOutput() { @@ -45,14 +47,24 @@ private String getOutput() { TransformationInfo info = new TransformationInfo(); info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("application/n-triples"); QC1 = info; } - Transformation transformation; + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("text/turtle"); + QC1_TTL = info; + } + + SparqlTransformation transformation; @BeforeEach public void setup() { transformation = new SparqlTransformation(); + transformation.serializer = new Serializer(); } @Test @@ -85,4 +97,16 @@ public void testConstructQuery() assertTrue(getOutput().startsWith("")); assertEquals(1, getOutput().lines().count()); } + + @Test + public void testConstructQuerySerializeTurtle() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1_TTL, CONFIG); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + // assertEquals("", getOutput()); + assertTrue(getOutput().startsWith("PREFIX rdf")); + assertEquals(5, getOutput().lines().count()); + } } From 90e8bfd22f5c515d070a92ea8c3b68ddf9bda4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 15:19:18 +0200 Subject: [PATCH 04/31] implements method for getting output media type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index 2118b3f..02482f6 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -127,6 +127,6 @@ public Uni transformAsync( @Override public String getOutputMediaType() { - return ""; + return transformationInfo.getMediaType(); } } From 8f5c5b7731be7cead0109b09ee76213d34f3259c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 18:00:57 +0200 Subject: [PATCH 05/31] adds content negotiation to SPARQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/seed-xc-sparql/pom.xml | 5 ++ .../ulbms/scdh/seed/xc/jena/Serializer.java | 53 ++++++++++++++---- .../seed/xc/jena/SparqlTransformation.java | 11 ++-- .../scdh/seed/xc/jena/SerializerTest.java | 54 +++++++++++++++++++ .../xc/jena/SparqlTransformationTest.java | 14 +++-- 5 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java diff --git a/plugins/seed-xc-sparql/pom.xml b/plugins/seed-xc-sparql/pom.xml index ffa9e61..b122817 100644 --- a/plugins/seed-xc-sparql/pom.xml +++ b/plugins/seed-xc-sparql/pom.xml @@ -45,6 +45,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-junit5-mockito + test + diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java index c81a790..fd6fc8e 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java @@ -1,5 +1,8 @@ package de.ulbms.scdh.seed.xc.jena; +import de.ulbms.scdh.seed.xc.api.TransformationPreparationException; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; import jakarta.enterprise.context.ApplicationScoped; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFFormat; @@ -14,18 +17,50 @@ public class Serializer { public static final Lang DEFAULT = Lang.N3; - public static RDFFormat getFormat(String transformationContentType, String systemId) { - if (transformationContentType != null) { - Lang lang = RDFLanguages.contentTypeToLang(transformationContentType); - if (lang.equals(Lang.NTRIPLES)) { - return new RDFFormat(lang, RDFFormat.UTF8); - } else if (lang.equals(Lang.TTL)) { - return new RDFFormat(lang, RDFFormat.PRETTY); + /** + * This implementation prefers the content type declared for the transformation (first argument) + * over the accept header of the request. If none is given, the content type is guessed from the + * processed file extension. DEFAULT is returned as fallback. + * + * @param transformationContentType - the content type declared for the transformation + * @param systemId - the name of the request file + * @param request - HTTP request with accept headers + * @return the RDF content type + */ + public static RDFFormat getFormat(String transformationContentType, String systemId, HttpServerRequest request) + throws TransformationPreparationException { + try { + if (transformationContentType != null) { + Lang lang = RDFLanguages.contentTypeToLang(transformationContentType); + return getFormatVariant(lang, "utf-8"); + } else if (request != null & request.getHeader(HttpHeaders.ACCEPT) != null) { + Lang lang = RDFLanguages.contentTypeToLang(request.getHeader(HttpHeaders.ACCEPT)); + return getFormatVariant(lang, request.getHeader(HttpHeaders.ACCEPT_CHARSET)); } else { - return new RDFFormat(lang); + return new RDFFormat(RDFLanguages.filenameToLang(systemId, DEFAULT)); } + } catch (Exception e) { + throw new TransformationPreparationException( + "unknown RDF format: " + transformationContentType + " " + request.getHeader(HttpHeaders.ACCEPT)); + } + } + + /** + * This adds missing information to get a format variant. + * + * @param lang - the basic format as {@link Lang} + * @param charset - the charset requested + * @return - the fully specified format for which a formatter exists + */ + protected static RDFFormat getFormatVariant(Lang lang, String charset) { + if (lang.equals(Lang.NTRIPLES)) { + return new RDFFormat(lang, RDFFormat.UTF8); + } else if (lang.equals(Lang.TTL)) { + return new RDFFormat(lang, RDFFormat.PRETTY); + } else if (lang.equals(Lang.RDFXML)) { + return new RDFFormat(lang, RDFFormat.PLAIN); } else { - return new RDFFormat(RDFLanguages.filenameToLang(systemId, DEFAULT)); + return new RDFFormat(lang); } } } diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index 02482f6..106157f 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -2,6 +2,7 @@ import de.ulbms.scdh.seed.xc.api.*; import io.smallrye.mutiny.Uni; +import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; @@ -68,7 +69,8 @@ public byte[] transform( Config config, String systemId, InputStream source, - ResourceProvider resourceProvider) + ResourceProvider resourceProvider, + HttpServerRequest request) throws TransformationPreparationException, TransformationException { try { // make graph @@ -97,7 +99,7 @@ public byte[] transform( qexec.close(); // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); - RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId); + RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); RDFDataMgr.write(output, resultModel, format); return output.toByteArray(); } catch (RiotException e) { @@ -115,10 +117,11 @@ public Uni transformAsync( Config config, String systemId, Uni source, - ResourceProvider resourceProvider) { + ResourceProvider resourceProvider, + HttpServerRequest request) { return source.onItem().transform((sourceStream) -> { try { - return transform(parameters, config, systemId, sourceStream, resourceProvider); + return transform(parameters, config, systemId, sourceStream, resourceProvider, request); } catch (TransformationPreparationException | TransformationException e) { throw new InternalServerErrorException(e.getMessage()); } diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java new file mode 100644 index 0000000..ec9046f --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java @@ -0,0 +1,54 @@ +package de.ulbms.scdh.seed.xc.jena; + +import static org.junit.jupiter.api.Assertions.*; + +import de.ulbms.scdh.seed.xc.api.TransformationPreparationException; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import org.apache.jena.riot.RDFFormat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@QuarkusTest +class SerializerTest { + + @InjectMock + HttpServerRequest request; + + @BeforeEach + public void setup() { + Mockito.when(request.getHeader(HttpHeaders.ACCEPT)).thenReturn("application/rdf+xml"); + } + + @Test + public void testInfoTurtle() throws TransformationPreparationException { + assertEquals(RDFFormat.TURTLE_PRETTY, Serializer.getFormat("text/turtle", null, request)); + } + + @Test + public void testInfoXML() throws TransformationPreparationException { + assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat("application/rdf+xml", null, request)); + } + + @Test + public void testNoInfo() throws TransformationPreparationException { + assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat(null, null, request)); + } + + @Test + public void testInfoYaml() throws TransformationPreparationException { + assertThrows( + TransformationPreparationException.class, + () -> Serializer.getFormat("application/rdf+yaml", null, request)); + } + + @Disabled("cannot set request to null while mocking") + @Test + public void testFromSystemId() throws TransformationPreparationException { + assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat(null, "data.rdf", null)); + } +} diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java index b4c9103..f5f7aa1 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import de.ulbms.scdh.seed.xc.api.*; +import io.vertx.core.http.HttpServerRequest; +import jakarta.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -30,6 +32,9 @@ class SparqlTransformationTest { private static TransformationInfo QC1_TTL; + @Inject + HttpServerRequest request; + private byte[] output; private String getOutput() { @@ -73,7 +78,8 @@ public void testNoSystemIdNonDefaultLang() FileNotFoundException { transformation.setup(QS1, CONFIG); InputStream in = new FileInputStream(VCDB1); - assertThrows(TransformationException.class, () -> transformation.transform(null, null, null, in, null)); + assertThrows( + TransformationException.class, () -> transformation.transform(null, null, null, in, null, request)); } @Test @@ -84,7 +90,7 @@ public void testSelectQuery() InputStream in = new FileInputStream(VCDB1); assertThrows( TransformationException.class, - () -> transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null)); + () -> transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request)); } @Test @@ -93,7 +99,7 @@ public void testConstructQuery() FileNotFoundException { transformation.setup(QC1, CONFIG); InputStream in = new FileInputStream(VCDB1); - output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); assertTrue(getOutput().startsWith("")); assertEquals(1, getOutput().lines().count()); } @@ -104,7 +110,7 @@ public void testConstructQuerySerializeTurtle() FileNotFoundException { transformation.setup(QC1_TTL, CONFIG); InputStream in = new FileInputStream(VCDB1); - output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); // assertEquals("", getOutput()); assertTrue(getOutput().startsWith("PREFIX rdf")); assertEquals(5, getOutput().lines().count()); From 2fe9773ee96b2ce8b90ac10ec71d7c0d48e949e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 18:12:43 +0200 Subject: [PATCH 06/31] renamed to SparqlConstruct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- ...parqlTransformation.java => SparqlConstruct.java} | 10 +++++----- .../de.ulbms.scdh.seed.xc.api.Transformation | 2 +- ...nsformationTest.java => SparqlConstructTest.java} | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) rename plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/{SparqlTransformation.java => SparqlConstruct.java} (93%) rename plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/{SparqlTransformationTest.java => SparqlConstructTest.java} (91%) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java similarity index 93% rename from plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java rename to plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 106157f..7ff298c 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -16,14 +16,14 @@ import org.slf4j.LoggerFactory; /** - * The {@link SparqlTransformation} is a {@link Transformation} + * The {@link SparqlConstruct} is a {@link Transformation} * plugin for running SPARQL queries. */ -public class SparqlTransformation implements Transformation { +public class SparqlConstruct implements Transformation { - private static final Logger LOG = LoggerFactory.getLogger(SparqlTransformation.class); + private static final Logger LOG = LoggerFactory.getLogger(SparqlConstruct.class); - public static final String TRANSFORMATION_TYPE = "sparql"; + public static final String TRANSFORMATION_TYPE = "sparql-construct"; TransformationInfo transformationInfo; @@ -37,7 +37,7 @@ public class SparqlTransformation implements Transformation { @Override public String getType() { - return SparqlTransformation.TRANSFORMATION_TYPE; + return SparqlConstruct.TRANSFORMATION_TYPE; } @Override diff --git a/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation index b002e95..f39e086 100644 --- a/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation +++ b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation @@ -1 +1 @@ -de.ulbms.scdh.seed.xc.jena.SparqlTransformation \ No newline at end of file +de.ulbms.scdh.seed.xc.jena.SparqlConstruct \ No newline at end of file diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java similarity index 91% rename from plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java rename to plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index f5f7aa1..634f128 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class SparqlTransformationTest { +class SparqlConstructTest { private static final File DATA_DIR = Paths.get("src", "test", "resources", "data").toFile(); @@ -43,14 +43,14 @@ private String getOutput() { static { TransformationInfo info = new TransformationInfo(); - info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qs1.rq").getAbsolutePath()); QS1 = info; } static { TransformationInfo info = new TransformationInfo(); - info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); info.setMediaType("application/n-triples"); QC1 = info; @@ -58,17 +58,17 @@ private String getOutput() { static { TransformationInfo info = new TransformationInfo(); - info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); info.setMediaType("text/turtle"); QC1_TTL = info; } - SparqlTransformation transformation; + SparqlConstruct transformation; @BeforeEach public void setup() { - transformation = new SparqlTransformation(); + transformation = new SparqlConstruct(); transformation.serializer = new Serializer(); } From 39a444dc6cd290b82fc12e25e30f1d7102e5b13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 18:33:59 +0200 Subject: [PATCH 07/31] about the plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/seed-xc-sparql/README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/seed-xc-sparql/README.md b/plugins/seed-xc-sparql/README.md index 9fffdc0..be5af95 100644 --- a/plugins/seed-xc-sparql/README.md +++ b/plugins/seed-xc-sparql/README.md @@ -1,6 +1,23 @@ -# SPARQL Plugin +# SPARQL Plugins +This module provides transformation plugins. + +## Transformation Types + +- `sparql-construct` + +## Content Negotiation + +- If `TransformationInfo.mediaType` is present, then and only then it determines the returned format. +- If there's no content type on the transformation level and content negotiation by the request's `Accept` is used. +- In case no content type is defined on the transformation level nor one is requested, the content tpye of input resource will be used +- As a fallback, a default content type is used. ## Parameters -https://jena.apache.org/documentation/query/parameterized-sparql-strings.html \ No newline at end of file +Request parameters are passed to the query with a mechanism by +[Apache Jena](https://jena.apache.org/documentation/query/parameterized-sparql-strings.html). +It replaces all occurrences of a SPARQL variable `?X` with a literal. + +Parameter types should be declared with the parameter descripter as `xs:...` types. +`xs:string` is used as default. \ No newline at end of file From e64276b66e7fd3c09f2b77a14e3cdab2fee9e455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Apr 2026 00:08:01 +0200 Subject: [PATCH 08/31] adds JSON-LD framing with Titanium MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../ulbms/scdh/seed/xc/jena/Serializer.java | 2 ++ .../scdh/seed/xc/jena/SparqlConstruct.java | 29 +++++++++++++-- .../scdh/seed/xc/jena/SerializerTest.java | 8 +++++ .../seed/xc/jena/SparqlConstructTest.java | 36 +++++++++++++++++++ .../src/test/resources/context/person.json | 8 +++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/test/resources/context/person.json diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java index fd6fc8e..c312329 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java @@ -59,6 +59,8 @@ protected static RDFFormat getFormatVariant(Lang lang, String charset) { return new RDFFormat(lang, RDFFormat.PRETTY); } else if (lang.equals(Lang.RDFXML)) { return new RDFFormat(lang, RDFFormat.PLAIN); + } else if (lang.equals(Lang.JSONLD)) { + return RDFFormat.JSONLD11_PRETTY; } else { return new RDFFormat(lang); } diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 7ff298c..7d28216 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -1,9 +1,14 @@ package de.ulbms.scdh.seed.xc.jena; +import com.apicatalog.jsonld.JsonLd; +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.JsonLdOptions; +import com.apicatalog.jsonld.document.JsonDocument; import de.ulbms.scdh.seed.xc.api.*; import io.smallrye.mutiny.Uni; import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; +import jakarta.json.*; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; import java.nio.file.Files; @@ -12,6 +17,9 @@ import org.apache.jena.query.*; import org.apache.jena.rdf.model.Model; import org.apache.jena.riot.*; +import org.apache.jena.riot.system.jsonld.JenaToTitanium; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +33,8 @@ public class SparqlConstruct implements Transformation { public static final String TRANSFORMATION_TYPE = "sparql-construct"; + String frame; + TransformationInfo transformationInfo; String query; @@ -100,14 +110,29 @@ public byte[] transform( // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); - RDFDataMgr.write(output, resultModel, format); - return output.toByteArray(); + if (!format.getLang().equals(Lang.JSONLD11) || frame == null) { + RDFDataMgr.write(output, resultModel, format); + return output.toByteArray(); + } else { + // use titanium for framing + JsonLdOptions opts = new JsonLdOptions(); + DatasetGraph dsg = DatasetGraphFactory.create(resultModel.getGraph()); + JsonArray ja = JenaToTitanium.convert(dsg, opts); + JsonDocument jdoc = JsonDocument.of(ja); + JsonObject framed = JsonLd.frame(jdoc, frame).get(); + JsonWriter writer = Json.createWriter(output); + writer.writeObject(framed); + return output.toByteArray(); + } } catch (RiotException e) { LOG.error("failed to read RDF dataset {}", e.getMessage()); throw new TransformationException(e); } catch (QueryExecException e) { LOG.error("failed to execute SPARQL query: {}", e.getMessage()); throw new TransformationException(e); + } catch (JsonLdError e) { + LOG.error("failed to load into titanium json-ld, {}", e.getMessage()); + throw new TransformationException(e); } } diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java index ec9046f..2003850 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java @@ -7,6 +7,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; +import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFFormat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -51,4 +52,11 @@ public void testInfoYaml() throws TransformationPreparationException { public void testFromSystemId() throws TransformationPreparationException { assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat(null, "data.rdf", null)); } + + @Test + public void testJsonLD() throws TransformationPreparationException { + assertEquals( + Lang.JSONLD11, + Serializer.getFormat("application/ld+json", null, request).getLang()); + } } diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index 634f128..cdd49cb 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -24,6 +24,9 @@ class SparqlConstructTest { private static final File RQ_DIR = Paths.get("src", "test", "resources", "rq").toFile(); + private static final File FRAME = + Paths.get("src", "test", "resources", "context", "person.json").toFile(); + private static final File CONFIG = RQ_DIR; private static TransformationInfo QS1; @@ -32,6 +35,8 @@ class SparqlConstructTest { private static TransformationInfo QC1_TTL; + private static TransformationInfo QC1_JSONLD; + @Inject HttpServerRequest request; @@ -64,6 +69,14 @@ private String getOutput() { QC1_TTL = info; } + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("application/ld+json"); + QC1_JSONLD = info; + } + SparqlConstruct transformation; @BeforeEach @@ -115,4 +128,27 @@ public void testConstructQuerySerializeTurtle() assertTrue(getOutput().startsWith("PREFIX rdf")); assertEquals(5, getOutput().lines().count()); } + + @Test + public void testConstructQuerySerializeJsonLd() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1_JSONLD, CONFIG); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); + assertTrue(getOutput().contains("\"@id\": \"http://somewhere/JohnSmith\"")); + // assertEquals("", getOutput()); + } + + @Test + public void testConstructQuerySerializeFraming() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1_JSONLD, CONFIG); + transformation.frame = FRAME.getAbsoluteFile().toURI().toString(); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); + assertTrue(getOutput().contains("\"id\":\"here:JohnSmith\"")); + // assertEquals("", getOutput()); + } } diff --git a/plugins/seed-xc-sparql/src/test/resources/context/person.json b/plugins/seed-xc-sparql/src/test/resources/context/person.json new file mode 100644 index 0000000..765acfa --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/context/person.json @@ -0,0 +1,8 @@ +{ + "@context": { + "here": "http://somewhere/", + "vCard": "http://www.w3.org/2001/vcard-rdf/3.0#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "id": { "@type": "@id", "@id": "@id" } + } +} \ No newline at end of file From f9d4de9af4ae43048687192c5352d5f8c543e67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 11:03:40 +0200 Subject: [PATCH 09/31] makes JSON-LD context for framing configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../resources/openapi/seed-xc-openapi.yaml | 12 +++++++++++ .../scdh/seed/xc/jena/SparqlConstruct.java | 21 +++++++++++++++---- .../seed/xc/jena/SparqlConstructTest.java | 18 +++++++++++----- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/api/src/main/resources/openapi/seed-xc-openapi.yaml b/api/src/main/resources/openapi/seed-xc-openapi.yaml index 9ba5799..2a957ad 100644 --- a/api/src/main/resources/openapi/seed-xc-openapi.yaml +++ b/api/src/main/resources/openapi/seed-xc-openapi.yaml @@ -417,6 +417,16 @@ components: type: type: string description: The local name of the parameter type. Write 'integer' instead of 'xs:integer'! + context: + description: Options of the JSON-LD context + type: object + required: + - location + properties: + location: + type: string + format: uri + description: The URI of the initial active context transformationInfo: description: Information about the transformation resource and its runtime parameters type: object @@ -490,6 +500,8 @@ components: $ref: '#/components/schemas/parser' serializer: $ref: '#/components/schemas/serializer' + context: + $ref: '#/components/schemas/context' required: - class - description diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 7d28216..4cc2293 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -11,6 +11,7 @@ import jakarta.json.*; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -33,8 +34,6 @@ public class SparqlConstruct implements Transformation { public static final String TRANSFORMATION_TYPE = "sparql-construct"; - String frame; - TransformationInfo transformationInfo; String query; @@ -73,6 +72,20 @@ public XsltParameterDetails getTransformationParameters() { return new XsltParameterDetails(); } + /** + * Returns the configured context location or null when there is none. + * @return - {@link URI} to the context location + */ + private URI getContextUri() { + URI result; + if (transformationInfo.getContext() == null) { + result = null; + } else { + result = transformationInfo.getContext().getLocation(); + } + return result; + } + @Override public byte[] transform( RuntimeParameters parameters, @@ -110,7 +123,7 @@ public byte[] transform( // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); - if (!format.getLang().equals(Lang.JSONLD11) || frame == null) { + if (!format.getLang().equals(Lang.JSONLD11) || getContextUri() == null) { RDFDataMgr.write(output, resultModel, format); return output.toByteArray(); } else { @@ -119,7 +132,7 @@ public byte[] transform( DatasetGraph dsg = DatasetGraphFactory.create(resultModel.getGraph()); JsonArray ja = JenaToTitanium.convert(dsg, opts); JsonDocument jdoc = JsonDocument.of(ja); - JsonObject framed = JsonLd.frame(jdoc, frame).get(); + JsonObject framed = JsonLd.frame(jdoc, getContextUri()).get(); JsonWriter writer = Json.createWriter(output); writer.writeObject(framed); return output.toByteArray(); diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index cdd49cb..c25fa6f 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -37,6 +37,8 @@ class SparqlConstructTest { private static TransformationInfo QC1_JSONLD; + private static TransformationInfo QC1_JSONLD_WITH_CONTEXT; + @Inject HttpServerRequest request; @@ -77,6 +79,15 @@ private String getOutput() { QC1_JSONLD = info; } + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("application/ld+json"); + info.setContext(new Context(FRAME.getAbsoluteFile().toURI())); + QC1_JSONLD_WITH_CONTEXT = info; + } + SparqlConstruct transformation; @BeforeEach @@ -124,7 +135,6 @@ public void testConstructQuerySerializeTurtle() transformation.setup(QC1_TTL, CONFIG); InputStream in = new FileInputStream(VCDB1); output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); - // assertEquals("", getOutput()); assertTrue(getOutput().startsWith("PREFIX rdf")); assertEquals(5, getOutput().lines().count()); } @@ -137,18 +147,16 @@ public void testConstructQuerySerializeJsonLd() InputStream in = new FileInputStream(VCDB1); output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); assertTrue(getOutput().contains("\"@id\": \"http://somewhere/JohnSmith\"")); - // assertEquals("", getOutput()); } @Test public void testConstructQuerySerializeFraming() throws ConfigurationException, TransformationPreparationException, TransformationException, FileNotFoundException { - transformation.setup(QC1_JSONLD, CONFIG); - transformation.frame = FRAME.getAbsoluteFile().toURI().toString(); + transformation.setup(QC1_JSONLD_WITH_CONTEXT, CONFIG); InputStream in = new FileInputStream(VCDB1); output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); assertTrue(getOutput().contains("\"id\":\"here:JohnSmith\"")); - // assertEquals("", getOutput()); + assertFalse(getOutput().contains("\"@id\":\"here:JohnSmith\"")); } } From 264629e6873daa11a4b14679d8929e483dbee433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 12:16:18 +0200 Subject: [PATCH 10/31] encapsulates preparation of the JSON-LD framing context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../scdh/seed/xc/jena/SparqlConstruct.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 4cc2293..27a078c 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -3,6 +3,7 @@ import com.apicatalog.jsonld.JsonLd; import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.JsonLdOptions; +import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.document.JsonDocument; import de.ulbms.scdh.seed.xc.api.*; import io.smallrye.mutiny.Uni; @@ -86,6 +87,25 @@ private URI getContextUri() { return result; } + /** + * Returns the JSON-LD context as a {@link Document}. This method encapsulates + * the preparation of the context including all options, e.g. setting a timeout. + * @return the context {@link Document} + * @throws TransformationPreparationException when preparation failed + */ + private Document getContext() throws TransformationPreparationException { + try { + return JsonDocument.of(getContextUri().toURL().openStream()); + } catch (JsonLdError e) { + LOG.error("failed to read JSON-LD framing context {}", getContextUri()); + throw new TransformationPreparationException( + "failed to read JSON-LD framing context " + getContextUri(), e); + } catch (IOException | NullPointerException e) { + LOG.error("JSON-LD framing URI not found {}", getContextUri()); + throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); + } + } + @Override public byte[] transform( RuntimeParameters parameters, @@ -132,7 +152,7 @@ public byte[] transform( DatasetGraph dsg = DatasetGraphFactory.create(resultModel.getGraph()); JsonArray ja = JenaToTitanium.convert(dsg, opts); JsonDocument jdoc = JsonDocument.of(ja); - JsonObject framed = JsonLd.frame(jdoc, getContextUri()).get(); + JsonObject framed = JsonLd.frame(jdoc, getContext()).get(); JsonWriter writer = Json.createWriter(output); writer.writeObject(framed); return output.toByteArray(); @@ -144,7 +164,7 @@ public byte[] transform( LOG.error("failed to execute SPARQL query: {}", e.getMessage()); throw new TransformationException(e); } catch (JsonLdError e) { - LOG.error("failed to load into titanium json-ld, {}", e.getMessage()); + LOG.error("JSON-LD processing failed, {}", e.getMessage()); throw new TransformationException(e); } } From 49823aa1b101ba42e4007387f049834b3456651f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 14:48:54 +0200 Subject: [PATCH 11/31] fixes resource management when getting context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../scdh/seed/xc/jena/SparqlConstruct.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 27a078c..b0d2085 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -12,7 +12,9 @@ import jakarta.json.*; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; +import java.net.SocketTimeoutException; import java.net.URI; +import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -22,6 +24,7 @@ import org.apache.jena.riot.system.jsonld.JenaToTitanium; import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +38,12 @@ public class SparqlConstruct implements Transformation { public static final String TRANSFORMATION_TYPE = "sparql-construct"; + @ConfigProperty(name = "url-connect-timeout", defaultValue = "10000") + int contextConnectTimeout; + + @ConfigProperty(name = "url-read-timeout", defaultValue = "10000") + int contextReadTimeout; + TransformationInfo transformationInfo; String query; @@ -94,15 +103,32 @@ private URI getContextUri() { * @throws TransformationPreparationException when preparation failed */ private Document getContext() throws TransformationPreparationException { + InputStream in = null; try { - return JsonDocument.of(getContextUri().toURL().openStream()); + URLConnection connection = getContextUri().toURL().openConnection(); + connection.setConnectTimeout(contextConnectTimeout); + connection.setReadTimeout(contextReadTimeout); + in = connection.getInputStream(); + return JsonDocument.of(in); } catch (JsonLdError e) { LOG.error("failed to read JSON-LD framing context {}", getContextUri()); throw new TransformationPreparationException( "failed to read JSON-LD framing context " + getContextUri(), e); + } catch (SocketTimeoutException e) { + LOG.warn("timeout when reading JSON-LD context from {}", getContextUri()); + throw new TransformationPreparationException( + "timeout when reading JSON-LD context from " + getContextUri(), e); } catch (IOException | NullPointerException e) { LOG.error("JSON-LD framing URI not found {}", getContextUri()); throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } From b0dd4bc05b0334dba3a626a32c1aecfcf06ab14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 14:53:24 +0200 Subject: [PATCH 12/31] make SPARQL plugin available in DTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- dts/pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dts/pom.xml b/dts/pom.xml index 794a769..cc7fbe7 100644 --- a/dts/pom.xml +++ b/dts/pom.xml @@ -33,12 +33,22 @@ seed-xc-transformations ${revision}${changelist} + ${project.groupId} seed-xc-saxon ${revision}${changelist} ${plugins.scope} + + ${project.groupId} + seed-xc-sparql + ${revision}${changelist} + ${plugins.scope} + ${project.groupId} seed-resource-providers From c749554abe6f5dcb76dea4c8a81de2297639c62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 17:05:19 +0200 Subject: [PATCH 13/31] return after branching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index b0d2085..2f89d0d 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -171,7 +171,6 @@ public byte[] transform( RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); if (!format.getLang().equals(Lang.JSONLD11) || getContextUri() == null) { RDFDataMgr.write(output, resultModel, format); - return output.toByteArray(); } else { // use titanium for framing JsonLdOptions opts = new JsonLdOptions(); @@ -181,8 +180,8 @@ public byte[] transform( JsonObject framed = JsonLd.frame(jdoc, getContext()).get(); JsonWriter writer = Json.createWriter(output); writer.writeObject(framed); - return output.toByteArray(); } + return output.toByteArray(); } catch (RiotException e) { LOG.error("failed to read RDF dataset {}", e.getMessage()); throw new TransformationException(e); From be1f810051c0465121b758c0ef04eee295af02b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 17:56:37 +0200 Subject: [PATCH 14/31] introduces size limit for JSON-LD context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../scdh/seed/xc/jena/SparqlConstruct.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 2f89d0d..180e370 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -44,6 +44,9 @@ public class SparqlConstruct implements Transformation { @ConfigProperty(name = "url-read-timeout", defaultValue = "10000") int contextReadTimeout; + @ConfigProperty(name = "context-max-size", defaultValue = "1048576") + long contextMaxSize; + TransformationInfo transformationInfo; String query; @@ -103,32 +106,29 @@ private URI getContextUri() { * @throws TransformationPreparationException when preparation failed */ private Document getContext() throws TransformationPreparationException { - InputStream in = null; + URLConnection connection; try { - URLConnection connection = getContextUri().toURL().openConnection(); + // toURL cannot not cause NPE because of way this method is used + connection = getContextUri().toURL().openConnection(); connection.setConnectTimeout(contextConnectTimeout); connection.setReadTimeout(contextReadTimeout); - in = connection.getInputStream(); + } catch (IOException | NullPointerException e) { + LOG.error("JSON-LD framing URI not found {}", getContextUri()); + throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); + } + try (InputStream in = connection.getInputStream()) { + if (contextMaxSize != 0 && connection.getContentLengthLong() > contextMaxSize) { + throw new TransformationPreparationException("context exceeds size limit"); + } return JsonDocument.of(in); - } catch (JsonLdError e) { - LOG.error("failed to read JSON-LD framing context {}", getContextUri()); - throw new TransformationPreparationException( - "failed to read JSON-LD framing context " + getContextUri(), e); } catch (SocketTimeoutException e) { LOG.warn("timeout when reading JSON-LD context from {}", getContextUri()); throw new TransformationPreparationException( "timeout when reading JSON-LD context from " + getContextUri(), e); - } catch (IOException | NullPointerException e) { - LOG.error("JSON-LD framing URI not found {}", getContextUri()); - throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } + } catch (JsonLdError | IOException e) { + LOG.error("failed to read JSON-LD framing context {}", getContextUri()); + throw new TransformationPreparationException( + "failed to read JSON-LD framing context " + getContextUri(), e); } } From 144adc3ef18021e0de8a27140c17c23d9cba9862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 17:56:58 +0200 Subject: [PATCH 15/31] about the plugin's application properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/seed-xc-sparql/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/seed-xc-sparql/README.md b/plugins/seed-xc-sparql/README.md index be5af95..8cd6608 100644 --- a/plugins/seed-xc-sparql/README.md +++ b/plugins/seed-xc-sparql/README.md @@ -1,6 +1,12 @@ # SPARQL Plugins -This module provides transformation plugins. +This module provides transformation plugins. It is based on +[Apache Jena](https://jena.apache.org/) and uses +[Titanium JSON-LD](https://github.com/filip26/titanium-json-ld) +for JSON-LD framing. + +For JSON-LD contexts used in framing, this plugin allows outbound +URL connections passing by the `ResourceProvider`. ## Transformation Types @@ -13,11 +19,17 @@ This module provides transformation plugins. - In case no content type is defined on the transformation level nor one is requested, the content tpye of input resource will be used - As a fallback, a default content type is used. -## Parameters +## Transformation Parameters Request parameters are passed to the query with a mechanism by [Apache Jena](https://jena.apache.org/documentation/query/parameterized-sparql-strings.html). It replaces all occurrences of a SPARQL variable `?X` with a literal. Parameter types should be declared with the parameter descripter as `xs:...` types. -`xs:string` is used as default. \ No newline at end of file +`xs:string` is used as default. + +## Application Properties + +- `url-connect-timeout`: time limit for establishing a connection to a remote JSON-LD context URL for framing, defaults to 10s +- `url-read-timeout`: time limit for fetching a remote JSON-LD context for framing, defaults to 10s +- `context-max-size`: size limit of JSON-LD context for framing, defaults to 1MB From f03da5d8be81cb4ed87c6bfd382261cc2f9552e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Apr 2026 22:44:00 +0200 Subject: [PATCH 16/31] adds SPARQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/pom.xml | 1 + plugins/seed-xc-sparql/README.md | 6 + plugins/seed-xc-sparql/pom.xml | 104 +++++++++++++++ .../scdh/seed/xc/jena/ParameterConverter.java | 84 ++++++++++++ .../seed/xc/jena/SparqlTransformation.java | 120 ++++++++++++++++++ .../de.ulbms.scdh.seed.xc.api.Transformation | 1 + .../xc/jena/SparqlTransformationTest.java | 13 ++ 7 files changed, 329 insertions(+) create mode 100644 plugins/seed-xc-sparql/README.md create mode 100644 plugins/seed-xc-sparql/pom.xml create mode 100644 plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java create mode 100644 plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java create mode 100644 plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation create mode 100644 plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java diff --git a/plugins/pom.xml b/plugins/pom.xml index 335236b..3efd9b3 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -17,6 +17,7 @@ resource-providers saxon + seed-xc-sparql diff --git a/plugins/seed-xc-sparql/README.md b/plugins/seed-xc-sparql/README.md new file mode 100644 index 0000000..9fffdc0 --- /dev/null +++ b/plugins/seed-xc-sparql/README.md @@ -0,0 +1,6 @@ +# SPARQL Plugin + + +## Parameters + +https://jena.apache.org/documentation/query/parameterized-sparql-strings.html \ No newline at end of file diff --git a/plugins/seed-xc-sparql/pom.xml b/plugins/seed-xc-sparql/pom.xml new file mode 100644 index 0000000..ffa9e61 --- /dev/null +++ b/plugins/seed-xc-sparql/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + de.ulbms.scdh.seed.xc + seed-xc-plugins + ${revision}${changelist} + + + seed-xc-sparql + SEED XC SPARQL + A plugin for transformations build from SPARQL queries + + + 21 + 21 + UTF-8 + 6.0.0 + + + + + ${project.groupId} + seed-xc-api + ${revision}${changelist} + + + ${project.groupId} + seed-resource-providers + ${revision}${changelist} + test + + + org.apache.jena + jena-arq + ${jena.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + io.quarkus + quarkus-junit5 + test + + + + + + + + + io.smallrye + jandex-maven-plugin + ${smallrye-jandex-plugin.version} + + + make-index + + jandex + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.5.0 + + + + + flatten + + flatten + + process-resources + + + + flatten.clean + + clean + + clean + + + + + + + + diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java new file mode 100644 index 0000000..bbf76fc --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/ParameterConverter.java @@ -0,0 +1,84 @@ +package de.ulbms.scdh.seed.xc.jena; + +import de.ulbms.scdh.seed.xc.api.TransformationPreparationException; +import jakarta.enterprise.context.ApplicationScoped; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import org.apache.jena.query.ParameterizedSparqlString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ParameterConverter} class casts strings to Java objects based on a xs-type. + */ +@ApplicationScoped +public class ParameterConverter { + + private static final Logger LOG = LoggerFactory.getLogger(ParameterConverter.class); + + /** + * Sets the variable with name in the supplied query. + * + * @param name - Name of the SPARQL variable + * @param value - Value to be set + * @param type - type information + * @param query - the parametrized query + * @throws TransformationPreparationException - on a cast failure + */ + public void setQueryParameter(String name, String value, String type, ParameterizedSparqlString query) + throws TransformationPreparationException { + switch (type) { + case "xs:anyURI" -> query.setIri(name, value); + case "xs:string" -> query.setLiteral(name, value); + case "xs:integer" -> { + try { + query.setLiteral(name, Integer.parseInt(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to integer", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:long" -> { + try { + query.setLiteral(name, Long.parseLong(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to long", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:float" -> { + try { + query.setLiteral(name, Float.parseFloat(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to float", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:double" -> { + try { + query.setLiteral(name, Double.parseDouble(value)); + } catch (NumberFormatException e) { + LOG.error("failed to cast '{}' value of parameter {} to double", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + case "xs:date" -> { + try { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat(); + calendar.setTime(sdf.parse(value)); + query.setLiteral(name, calendar); + } catch (ParseException e) { + LOG.error("failed to cast '{}' value of parameter {} to calendar", value, name); + throw new TransformationPreparationException("failed to set parameter " + name, e); + } + } + default -> { + // defaults to string again + LOG.error("no valid type information for parameter {}: {}. Using string", name, type); + query.setLiteral(name, value); + } + } + } +} diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java new file mode 100644 index 0000000..7834df7 --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -0,0 +1,120 @@ +package de.ulbms.scdh.seed.xc.jena; + +import de.ulbms.scdh.seed.xc.api.*; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.InternalServerErrorException; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.jena.query.*; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SparqlTransformation} is a {@link Transformation} + * plugin for running SPARQL queries. + */ +public class SparqlTransformation implements Transformation { + + private static final Logger LOG = LoggerFactory.getLogger(SparqlTransformation.class); + + public static final String TRANSFORMATION_TYPE = "sparql"; + + TransformationInfo transformationInfo; + + String query; + + @Inject + ParameterConverter parameterConverter; + + @Override + public String getType() { + return SparqlTransformation.TRANSFORMATION_TYPE; + } + + @Override + public void setup(TransformationInfo transformationInfo, File path) throws ConfigurationException { + this.transformationInfo = transformationInfo; + Path configFile = Paths.get(path.toURI()).toAbsolutePath().normalize(); + Path queryFile = configFile.resolve(transformationInfo.getLocation()); + try { + query = Files.readString(queryFile); + } catch (IOException e) { + LOG.error("failed to read SPARQL query: {}", queryFile); + throw new ConfigurationException("failed to read SPARQL query: " + queryFile); + } + } + + @Override + public TransformationInfo getTransformationInfo() { + return transformationInfo; + } + + @Override + public XsltParameterDetails getTransformationParameters() { + return new XsltParameterDetails(); + } + + @Override + public byte[] transform( + RuntimeParameters parameters, + Config config, + String systemId, + InputStream source, + ResourceProvider resourceProvider) + throws TransformationPreparationException { + // make graph + Dataset graph = RDFParser.source(source).toDataset(); + // make query + ParameterizedSparqlString queryTemplate = new ParameterizedSparqlString(this.query); + if (parameters != null) { + for (String key : parameters.getGlobalParameters().keySet()) { + ParameterDescriptor descriptor = + transformationInfo.getParameterDescriptors().get(key); + String value = parameters.getGlobalParameters().get(key); + if (descriptor == null) { + // assume string + queryTemplate.setLiteral(key, value); + } else { + parameterConverter.setQueryParameter(key, value, descriptor.getType(), queryTemplate); + } + } + } + Query query = queryTemplate.asQuery(); + // execute query + QueryExecution qexec = QueryExecutionFactory.create(query, graph); + Model resultModel = qexec.execConstruct(); + qexec.close(); + // write result back to the wire + ByteArrayOutputStream output = new ByteArrayOutputStream(); + RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); + return output.toByteArray(); + } + + @Override + public Uni transformAsync( + RuntimeParameters parameters, + Config config, + String systemId, + Uni source, + ResourceProvider resourceProvider) { + return source.onItem().transform((sourceStream) -> { + try { + return transform(parameters, config, systemId, sourceStream, resourceProvider); + } catch (TransformationPreparationException e) { + throw new InternalServerErrorException(e.getMessage()); + } + }); + } + + @Override + public String getOutputMediaType() { + return ""; + } +} diff --git a/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation new file mode 100644 index 0000000..b002e95 --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation @@ -0,0 +1 @@ +de.ulbms.scdh.seed.xc.jena.SparqlTransformation \ No newline at end of file diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java new file mode 100644 index 0000000..aede1f2 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -0,0 +1,13 @@ +package de.ulbms.scdh.seed.xc.jena; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class SparqlTransformationTest { + + @Test + public void test() { + assertEquals("", ""); + } +} From 3246704d64ad63b88c5879b38393a224458b5129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 09:51:17 +0200 Subject: [PATCH 17/31] adds basic tests for SPARQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../seed/xc/jena/SparqlTransformation.java | 64 ++++++++------- .../xc/jena/SparqlTransformationTest.java | 79 ++++++++++++++++++- .../src/test/resources/data/vc-db-1.rdf | 37 +++++++++ .../src/test/resources/rq/qc1.rq | 6 ++ .../src/test/resources/rq/qs1.rq | 4 + 5 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf create mode 100644 plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq create mode 100644 plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index 7834df7..d37ba6e 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -10,9 +10,7 @@ import java.nio.file.Paths; import org.apache.jena.query.*; import org.apache.jena.rdf.model.Model; -import org.apache.jena.riot.RDFDataMgr; -import org.apache.jena.riot.RDFFormat; -import org.apache.jena.riot.RDFParser; +import org.apache.jena.riot.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,33 +66,43 @@ public byte[] transform( String systemId, InputStream source, ResourceProvider resourceProvider) - throws TransformationPreparationException { - // make graph - Dataset graph = RDFParser.source(source).toDataset(); - // make query - ParameterizedSparqlString queryTemplate = new ParameterizedSparqlString(this.query); - if (parameters != null) { - for (String key : parameters.getGlobalParameters().keySet()) { - ParameterDescriptor descriptor = - transformationInfo.getParameterDescriptors().get(key); - String value = parameters.getGlobalParameters().get(key); - if (descriptor == null) { - // assume string - queryTemplate.setLiteral(key, value); - } else { - parameterConverter.setQueryParameter(key, value, descriptor.getType(), queryTemplate); + throws TransformationPreparationException, TransformationException { + try { + // make graph + Lang lang = RDFLanguages.filenameToLang(systemId, Lang.N3); + LOG.info("trying to parse RDF data from {} as format {}", systemId, lang); + Dataset graph = RDFParser.source(source).lang(lang).toDataset(); + // make query + ParameterizedSparqlString queryTemplate = new ParameterizedSparqlString(this.query); + if (parameters != null) { + for (String key : parameters.getGlobalParameters().keySet()) { + ParameterDescriptor descriptor = + transformationInfo.getParameterDescriptors().get(key); + String value = parameters.getGlobalParameters().get(key); + if (descriptor == null) { + // assume string + queryTemplate.setLiteral(key, value); + } else { + parameterConverter.setQueryParameter(key, value, descriptor.getType(), queryTemplate); + } } } + Query query = queryTemplate.asQuery(); + // execute query + QueryExecution qexec = QueryExecutionFactory.create(query, graph); + Model resultModel = qexec.execConstruct(); + qexec.close(); + // write result back to the wire + ByteArrayOutputStream output = new ByteArrayOutputStream(); + RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); + return output.toByteArray(); + } catch (RiotException e) { + LOG.error("failed to read RDF dataset {}", e.getMessage()); + throw new TransformationException(e); + } catch (QueryExecException e) { + LOG.error("failed to execute SPARQL query: {}", e.getMessage()); + throw new TransformationException(e); } - Query query = queryTemplate.asQuery(); - // execute query - QueryExecution qexec = QueryExecutionFactory.create(query, graph); - Model resultModel = qexec.execConstruct(); - qexec.close(); - // write result back to the wire - ByteArrayOutputStream output = new ByteArrayOutputStream(); - RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); - return output.toByteArray(); } @Override @@ -107,7 +115,7 @@ public Uni transformAsync( return source.onItem().transform((sourceStream) -> { try { return transform(parameters, config, systemId, sourceStream, resourceProvider); - } catch (TransformationPreparationException e) { + } catch (TransformationPreparationException | TransformationException e) { throw new InternalServerErrorException(e.getMessage()); } }); diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java index aede1f2..5bc916c 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -2,12 +2,87 @@ import static org.junit.jupiter.api.Assertions.*; +import de.ulbms.scdh.seed.xc.api.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SparqlTransformationTest { + private static final File DATA_DIR = + Paths.get("src", "test", "resources", "data").toFile(); + + private static final File VCDB1 = new File(DATA_DIR, "vc-db-1.rdf"); + + private static final File RQ_DIR = + Paths.get("src", "test", "resources", "rq").toFile(); + + private static final File CONFIG = RQ_DIR; + + private static TransformationInfo QS1; + + private static TransformationInfo QC1; + + private byte[] output; + + private String getOutput() { + return new String(output, StandardCharsets.UTF_8); + } + + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qs1.rq").getAbsolutePath()); + QS1 = info; + } + + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + QC1 = info; + } + + Transformation transformation; + + @BeforeEach + public void setup() { + transformation = new SparqlTransformation(); + } + + @Test + public void testNoSystemIdNonDefaultLang() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QS1, CONFIG); + InputStream in = new FileInputStream(VCDB1); + assertThrows(TransformationException.class, () -> transformation.transform(null, null, null, in, null)); + } + + @Test + public void testSelectQuery() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QS1, CONFIG); + InputStream in = new FileInputStream(VCDB1); + assertThrows( + TransformationException.class, + () -> transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null)); + } + @Test - public void test() { - assertEquals("", ""); + public void testConstructQuery() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1, CONFIG); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + assertTrue(getOutput().startsWith("")); + assertEquals(1, getOutput().lines().count()); } } diff --git a/plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf b/plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf new file mode 100644 index 0000000..b57eb13 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/data/vc-db-1.rdf @@ -0,0 +1,37 @@ + + + + John Smith + + Smith + John + + + + + Becky Smith + + Smith + Rebecca + + + + + Sarah Jones + + Jones + Sarah + + + + + Matt Jones + + + + diff --git a/plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq b/plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq new file mode 100644 index 0000000..6128145 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/rq/qc1.rq @@ -0,0 +1,6 @@ +CONSTRUCT { + ?x "John Smith" . +} +WHERE + { ?x "John Smith" } + diff --git a/plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq b/plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq new file mode 100644 index 0000000..fe5a103 --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/rq/qs1.rq @@ -0,0 +1,4 @@ +SELECT ?x +WHERE + { ?x "John Smith" } + From 9875229b5e56108c11cdd85627a94a79178d862a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 13:29:14 +0200 Subject: [PATCH 18/31] adds Serializer bean for choosing working output format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../ulbms/scdh/seed/xc/jena/Serializer.java | 31 +++++++++++++++++++ .../seed/xc/jena/SparqlTransformation.java | 6 +++- .../xc/jena/SparqlTransformationTest.java | 26 +++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java new file mode 100644 index 0000000..c81a790 --- /dev/null +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java @@ -0,0 +1,31 @@ +package de.ulbms.scdh.seed.xc.jena; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFLanguages; + +/** + * The {@link Serializer} adds sufficient information for getting an RDF output format that works. + * This includes choosing an encoding variant. + */ +@ApplicationScoped +public class Serializer { + + public static final Lang DEFAULT = Lang.N3; + + public static RDFFormat getFormat(String transformationContentType, String systemId) { + if (transformationContentType != null) { + Lang lang = RDFLanguages.contentTypeToLang(transformationContentType); + if (lang.equals(Lang.NTRIPLES)) { + return new RDFFormat(lang, RDFFormat.UTF8); + } else if (lang.equals(Lang.TTL)) { + return new RDFFormat(lang, RDFFormat.PRETTY); + } else { + return new RDFFormat(lang); + } + } else { + return new RDFFormat(RDFLanguages.filenameToLang(systemId, DEFAULT)); + } + } +} diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index d37ba6e..2118b3f 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -31,6 +31,9 @@ public class SparqlTransformation implements Transformation { @Inject ParameterConverter parameterConverter; + @Inject + Serializer serializer; + @Override public String getType() { return SparqlTransformation.TRANSFORMATION_TYPE; @@ -94,7 +97,8 @@ public byte[] transform( qexec.close(); // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); - RDFDataMgr.write(output, resultModel, RDFFormat.NTRIPLES); + RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId); + RDFDataMgr.write(output, resultModel, format); return output.toByteArray(); } catch (RiotException e) { LOG.error("failed to read RDF dataset {}", e.getMessage()); diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java index 5bc916c..b4c9103 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -28,6 +28,8 @@ class SparqlTransformationTest { private static TransformationInfo QC1; + private static TransformationInfo QC1_TTL; + private byte[] output; private String getOutput() { @@ -45,14 +47,24 @@ private String getOutput() { TransformationInfo info = new TransformationInfo(); info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("application/n-triples"); QC1 = info; } - Transformation transformation; + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("text/turtle"); + QC1_TTL = info; + } + + SparqlTransformation transformation; @BeforeEach public void setup() { transformation = new SparqlTransformation(); + transformation.serializer = new Serializer(); } @Test @@ -85,4 +97,16 @@ public void testConstructQuery() assertTrue(getOutput().startsWith("")); assertEquals(1, getOutput().lines().count()); } + + @Test + public void testConstructQuerySerializeTurtle() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1_TTL, CONFIG); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + // assertEquals("", getOutput()); + assertTrue(getOutput().startsWith("PREFIX rdf")); + assertEquals(5, getOutput().lines().count()); + } } From 84eacb4aa8bcbf3fa0af12e4542435fc91f92d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 15:19:18 +0200 Subject: [PATCH 19/31] implements method for getting output media type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index 2118b3f..02482f6 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -127,6 +127,6 @@ public Uni transformAsync( @Override public String getOutputMediaType() { - return ""; + return transformationInfo.getMediaType(); } } From 06b8d5baa6b3d8952450d2d515276de455793c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 18:00:57 +0200 Subject: [PATCH 20/31] adds content negotiation to SPARQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/seed-xc-sparql/pom.xml | 5 ++ .../ulbms/scdh/seed/xc/jena/Serializer.java | 53 ++++++++++++++---- .../seed/xc/jena/SparqlTransformation.java | 11 ++-- .../scdh/seed/xc/jena/SerializerTest.java | 54 +++++++++++++++++++ .../xc/jena/SparqlTransformationTest.java | 14 +++-- 5 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java diff --git a/plugins/seed-xc-sparql/pom.xml b/plugins/seed-xc-sparql/pom.xml index ffa9e61..b122817 100644 --- a/plugins/seed-xc-sparql/pom.xml +++ b/plugins/seed-xc-sparql/pom.xml @@ -45,6 +45,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-junit5-mockito + test + diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java index c81a790..fd6fc8e 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java @@ -1,5 +1,8 @@ package de.ulbms.scdh.seed.xc.jena; +import de.ulbms.scdh.seed.xc.api.TransformationPreparationException; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; import jakarta.enterprise.context.ApplicationScoped; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFFormat; @@ -14,18 +17,50 @@ public class Serializer { public static final Lang DEFAULT = Lang.N3; - public static RDFFormat getFormat(String transformationContentType, String systemId) { - if (transformationContentType != null) { - Lang lang = RDFLanguages.contentTypeToLang(transformationContentType); - if (lang.equals(Lang.NTRIPLES)) { - return new RDFFormat(lang, RDFFormat.UTF8); - } else if (lang.equals(Lang.TTL)) { - return new RDFFormat(lang, RDFFormat.PRETTY); + /** + * This implementation prefers the content type declared for the transformation (first argument) + * over the accept header of the request. If none is given, the content type is guessed from the + * processed file extension. DEFAULT is returned as fallback. + * + * @param transformationContentType - the content type declared for the transformation + * @param systemId - the name of the request file + * @param request - HTTP request with accept headers + * @return the RDF content type + */ + public static RDFFormat getFormat(String transformationContentType, String systemId, HttpServerRequest request) + throws TransformationPreparationException { + try { + if (transformationContentType != null) { + Lang lang = RDFLanguages.contentTypeToLang(transformationContentType); + return getFormatVariant(lang, "utf-8"); + } else if (request != null & request.getHeader(HttpHeaders.ACCEPT) != null) { + Lang lang = RDFLanguages.contentTypeToLang(request.getHeader(HttpHeaders.ACCEPT)); + return getFormatVariant(lang, request.getHeader(HttpHeaders.ACCEPT_CHARSET)); } else { - return new RDFFormat(lang); + return new RDFFormat(RDFLanguages.filenameToLang(systemId, DEFAULT)); } + } catch (Exception e) { + throw new TransformationPreparationException( + "unknown RDF format: " + transformationContentType + " " + request.getHeader(HttpHeaders.ACCEPT)); + } + } + + /** + * This adds missing information to get a format variant. + * + * @param lang - the basic format as {@link Lang} + * @param charset - the charset requested + * @return - the fully specified format for which a formatter exists + */ + protected static RDFFormat getFormatVariant(Lang lang, String charset) { + if (lang.equals(Lang.NTRIPLES)) { + return new RDFFormat(lang, RDFFormat.UTF8); + } else if (lang.equals(Lang.TTL)) { + return new RDFFormat(lang, RDFFormat.PRETTY); + } else if (lang.equals(Lang.RDFXML)) { + return new RDFFormat(lang, RDFFormat.PLAIN); } else { - return new RDFFormat(RDFLanguages.filenameToLang(systemId, DEFAULT)); + return new RDFFormat(lang); } } } diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java index 02482f6..106157f 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java @@ -2,6 +2,7 @@ import de.ulbms.scdh.seed.xc.api.*; import io.smallrye.mutiny.Uni; +import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; @@ -68,7 +69,8 @@ public byte[] transform( Config config, String systemId, InputStream source, - ResourceProvider resourceProvider) + ResourceProvider resourceProvider, + HttpServerRequest request) throws TransformationPreparationException, TransformationException { try { // make graph @@ -97,7 +99,7 @@ public byte[] transform( qexec.close(); // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); - RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId); + RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); RDFDataMgr.write(output, resultModel, format); return output.toByteArray(); } catch (RiotException e) { @@ -115,10 +117,11 @@ public Uni transformAsync( Config config, String systemId, Uni source, - ResourceProvider resourceProvider) { + ResourceProvider resourceProvider, + HttpServerRequest request) { return source.onItem().transform((sourceStream) -> { try { - return transform(parameters, config, systemId, sourceStream, resourceProvider); + return transform(parameters, config, systemId, sourceStream, resourceProvider, request); } catch (TransformationPreparationException | TransformationException e) { throw new InternalServerErrorException(e.getMessage()); } diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java new file mode 100644 index 0000000..ec9046f --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java @@ -0,0 +1,54 @@ +package de.ulbms.scdh.seed.xc.jena; + +import static org.junit.jupiter.api.Assertions.*; + +import de.ulbms.scdh.seed.xc.api.TransformationPreparationException; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import org.apache.jena.riot.RDFFormat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@QuarkusTest +class SerializerTest { + + @InjectMock + HttpServerRequest request; + + @BeforeEach + public void setup() { + Mockito.when(request.getHeader(HttpHeaders.ACCEPT)).thenReturn("application/rdf+xml"); + } + + @Test + public void testInfoTurtle() throws TransformationPreparationException { + assertEquals(RDFFormat.TURTLE_PRETTY, Serializer.getFormat("text/turtle", null, request)); + } + + @Test + public void testInfoXML() throws TransformationPreparationException { + assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat("application/rdf+xml", null, request)); + } + + @Test + public void testNoInfo() throws TransformationPreparationException { + assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat(null, null, request)); + } + + @Test + public void testInfoYaml() throws TransformationPreparationException { + assertThrows( + TransformationPreparationException.class, + () -> Serializer.getFormat("application/rdf+yaml", null, request)); + } + + @Disabled("cannot set request to null while mocking") + @Test + public void testFromSystemId() throws TransformationPreparationException { + assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat(null, "data.rdf", null)); + } +} diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java index b4c9103..f5f7aa1 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import de.ulbms.scdh.seed.xc.api.*; +import io.vertx.core.http.HttpServerRequest; +import jakarta.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -30,6 +32,9 @@ class SparqlTransformationTest { private static TransformationInfo QC1_TTL; + @Inject + HttpServerRequest request; + private byte[] output; private String getOutput() { @@ -73,7 +78,8 @@ public void testNoSystemIdNonDefaultLang() FileNotFoundException { transformation.setup(QS1, CONFIG); InputStream in = new FileInputStream(VCDB1); - assertThrows(TransformationException.class, () -> transformation.transform(null, null, null, in, null)); + assertThrows( + TransformationException.class, () -> transformation.transform(null, null, null, in, null, request)); } @Test @@ -84,7 +90,7 @@ public void testSelectQuery() InputStream in = new FileInputStream(VCDB1); assertThrows( TransformationException.class, - () -> transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null)); + () -> transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request)); } @Test @@ -93,7 +99,7 @@ public void testConstructQuery() FileNotFoundException { transformation.setup(QC1, CONFIG); InputStream in = new FileInputStream(VCDB1); - output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); assertTrue(getOutput().startsWith("")); assertEquals(1, getOutput().lines().count()); } @@ -104,7 +110,7 @@ public void testConstructQuerySerializeTurtle() FileNotFoundException { transformation.setup(QC1_TTL, CONFIG); InputStream in = new FileInputStream(VCDB1); - output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); // assertEquals("", getOutput()); assertTrue(getOutput().startsWith("PREFIX rdf")); assertEquals(5, getOutput().lines().count()); From aac34754d51ae2de365ec8a3ccd46f8e16602050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 18:12:43 +0200 Subject: [PATCH 21/31] renamed to SparqlConstruct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- ...parqlTransformation.java => SparqlConstruct.java} | 10 +++++----- .../de.ulbms.scdh.seed.xc.api.Transformation | 2 +- ...nsformationTest.java => SparqlConstructTest.java} | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) rename plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/{SparqlTransformation.java => SparqlConstruct.java} (93%) rename plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/{SparqlTransformationTest.java => SparqlConstructTest.java} (91%) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java similarity index 93% rename from plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java rename to plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 106157f..7ff298c 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformation.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -16,14 +16,14 @@ import org.slf4j.LoggerFactory; /** - * The {@link SparqlTransformation} is a {@link Transformation} + * The {@link SparqlConstruct} is a {@link Transformation} * plugin for running SPARQL queries. */ -public class SparqlTransformation implements Transformation { +public class SparqlConstruct implements Transformation { - private static final Logger LOG = LoggerFactory.getLogger(SparqlTransformation.class); + private static final Logger LOG = LoggerFactory.getLogger(SparqlConstruct.class); - public static final String TRANSFORMATION_TYPE = "sparql"; + public static final String TRANSFORMATION_TYPE = "sparql-construct"; TransformationInfo transformationInfo; @@ -37,7 +37,7 @@ public class SparqlTransformation implements Transformation { @Override public String getType() { - return SparqlTransformation.TRANSFORMATION_TYPE; + return SparqlConstruct.TRANSFORMATION_TYPE; } @Override diff --git a/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation index b002e95..f39e086 100644 --- a/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation +++ b/plugins/seed-xc-sparql/src/main/resources/META-INF/services/de.ulbms.scdh.seed.xc.api.Transformation @@ -1 +1 @@ -de.ulbms.scdh.seed.xc.jena.SparqlTransformation \ No newline at end of file +de.ulbms.scdh.seed.xc.jena.SparqlConstruct \ No newline at end of file diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java similarity index 91% rename from plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java rename to plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index f5f7aa1..634f128 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlTransformationTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class SparqlTransformationTest { +class SparqlConstructTest { private static final File DATA_DIR = Paths.get("src", "test", "resources", "data").toFile(); @@ -43,14 +43,14 @@ private String getOutput() { static { TransformationInfo info = new TransformationInfo(); - info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qs1.rq").getAbsolutePath()); QS1 = info; } static { TransformationInfo info = new TransformationInfo(); - info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); info.setMediaType("application/n-triples"); QC1 = info; @@ -58,17 +58,17 @@ private String getOutput() { static { TransformationInfo info = new TransformationInfo(); - info.setPropertyClass(SparqlTransformation.TRANSFORMATION_TYPE); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); info.setMediaType("text/turtle"); QC1_TTL = info; } - SparqlTransformation transformation; + SparqlConstruct transformation; @BeforeEach public void setup() { - transformation = new SparqlTransformation(); + transformation = new SparqlConstruct(); transformation.serializer = new Serializer(); } From 76299015b28356f1ef83f9bdc3abc77ffa1a7f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Apr 2026 18:33:59 +0200 Subject: [PATCH 22/31] about the plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/seed-xc-sparql/README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/seed-xc-sparql/README.md b/plugins/seed-xc-sparql/README.md index 9fffdc0..be5af95 100644 --- a/plugins/seed-xc-sparql/README.md +++ b/plugins/seed-xc-sparql/README.md @@ -1,6 +1,23 @@ -# SPARQL Plugin +# SPARQL Plugins +This module provides transformation plugins. + +## Transformation Types + +- `sparql-construct` + +## Content Negotiation + +- If `TransformationInfo.mediaType` is present, then and only then it determines the returned format. +- If there's no content type on the transformation level and content negotiation by the request's `Accept` is used. +- In case no content type is defined on the transformation level nor one is requested, the content tpye of input resource will be used +- As a fallback, a default content type is used. ## Parameters -https://jena.apache.org/documentation/query/parameterized-sparql-strings.html \ No newline at end of file +Request parameters are passed to the query with a mechanism by +[Apache Jena](https://jena.apache.org/documentation/query/parameterized-sparql-strings.html). +It replaces all occurrences of a SPARQL variable `?X` with a literal. + +Parameter types should be declared with the parameter descripter as `xs:...` types. +`xs:string` is used as default. \ No newline at end of file From 6eea4fb396e60a885ab3cff02a476f4c9990807c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Apr 2026 00:08:01 +0200 Subject: [PATCH 23/31] adds JSON-LD framing with Titanium MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../ulbms/scdh/seed/xc/jena/Serializer.java | 2 ++ .../scdh/seed/xc/jena/SparqlConstruct.java | 29 +++++++++++++-- .../scdh/seed/xc/jena/SerializerTest.java | 8 +++++ .../seed/xc/jena/SparqlConstructTest.java | 36 +++++++++++++++++++ .../src/test/resources/context/person.json | 8 +++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 plugins/seed-xc-sparql/src/test/resources/context/person.json diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java index fd6fc8e..c312329 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/Serializer.java @@ -59,6 +59,8 @@ protected static RDFFormat getFormatVariant(Lang lang, String charset) { return new RDFFormat(lang, RDFFormat.PRETTY); } else if (lang.equals(Lang.RDFXML)) { return new RDFFormat(lang, RDFFormat.PLAIN); + } else if (lang.equals(Lang.JSONLD)) { + return RDFFormat.JSONLD11_PRETTY; } else { return new RDFFormat(lang); } diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 7ff298c..7d28216 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -1,9 +1,14 @@ package de.ulbms.scdh.seed.xc.jena; +import com.apicatalog.jsonld.JsonLd; +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.JsonLdOptions; +import com.apicatalog.jsonld.document.JsonDocument; import de.ulbms.scdh.seed.xc.api.*; import io.smallrye.mutiny.Uni; import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; +import jakarta.json.*; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; import java.nio.file.Files; @@ -12,6 +17,9 @@ import org.apache.jena.query.*; import org.apache.jena.rdf.model.Model; import org.apache.jena.riot.*; +import org.apache.jena.riot.system.jsonld.JenaToTitanium; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +33,8 @@ public class SparqlConstruct implements Transformation { public static final String TRANSFORMATION_TYPE = "sparql-construct"; + String frame; + TransformationInfo transformationInfo; String query; @@ -100,14 +110,29 @@ public byte[] transform( // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); - RDFDataMgr.write(output, resultModel, format); - return output.toByteArray(); + if (!format.getLang().equals(Lang.JSONLD11) || frame == null) { + RDFDataMgr.write(output, resultModel, format); + return output.toByteArray(); + } else { + // use titanium for framing + JsonLdOptions opts = new JsonLdOptions(); + DatasetGraph dsg = DatasetGraphFactory.create(resultModel.getGraph()); + JsonArray ja = JenaToTitanium.convert(dsg, opts); + JsonDocument jdoc = JsonDocument.of(ja); + JsonObject framed = JsonLd.frame(jdoc, frame).get(); + JsonWriter writer = Json.createWriter(output); + writer.writeObject(framed); + return output.toByteArray(); + } } catch (RiotException e) { LOG.error("failed to read RDF dataset {}", e.getMessage()); throw new TransformationException(e); } catch (QueryExecException e) { LOG.error("failed to execute SPARQL query: {}", e.getMessage()); throw new TransformationException(e); + } catch (JsonLdError e) { + LOG.error("failed to load into titanium json-ld, {}", e.getMessage()); + throw new TransformationException(e); } } diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java index ec9046f..2003850 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SerializerTest.java @@ -7,6 +7,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; +import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFFormat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -51,4 +52,11 @@ public void testInfoYaml() throws TransformationPreparationException { public void testFromSystemId() throws TransformationPreparationException { assertEquals(RDFFormat.RDFXML_PLAIN, Serializer.getFormat(null, "data.rdf", null)); } + + @Test + public void testJsonLD() throws TransformationPreparationException { + assertEquals( + Lang.JSONLD11, + Serializer.getFormat("application/ld+json", null, request).getLang()); + } } diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index 634f128..cdd49cb 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -24,6 +24,9 @@ class SparqlConstructTest { private static final File RQ_DIR = Paths.get("src", "test", "resources", "rq").toFile(); + private static final File FRAME = + Paths.get("src", "test", "resources", "context", "person.json").toFile(); + private static final File CONFIG = RQ_DIR; private static TransformationInfo QS1; @@ -32,6 +35,8 @@ class SparqlConstructTest { private static TransformationInfo QC1_TTL; + private static TransformationInfo QC1_JSONLD; + @Inject HttpServerRequest request; @@ -64,6 +69,14 @@ private String getOutput() { QC1_TTL = info; } + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("application/ld+json"); + QC1_JSONLD = info; + } + SparqlConstruct transformation; @BeforeEach @@ -115,4 +128,27 @@ public void testConstructQuerySerializeTurtle() assertTrue(getOutput().startsWith("PREFIX rdf")); assertEquals(5, getOutput().lines().count()); } + + @Test + public void testConstructQuerySerializeJsonLd() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1_JSONLD, CONFIG); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); + assertTrue(getOutput().contains("\"@id\": \"http://somewhere/JohnSmith\"")); + // assertEquals("", getOutput()); + } + + @Test + public void testConstructQuerySerializeFraming() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException { + transformation.setup(QC1_JSONLD, CONFIG); + transformation.frame = FRAME.getAbsoluteFile().toURI().toString(); + InputStream in = new FileInputStream(VCDB1); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); + assertTrue(getOutput().contains("\"id\":\"here:JohnSmith\"")); + // assertEquals("", getOutput()); + } } diff --git a/plugins/seed-xc-sparql/src/test/resources/context/person.json b/plugins/seed-xc-sparql/src/test/resources/context/person.json new file mode 100644 index 0000000..765acfa --- /dev/null +++ b/plugins/seed-xc-sparql/src/test/resources/context/person.json @@ -0,0 +1,8 @@ +{ + "@context": { + "here": "http://somewhere/", + "vCard": "http://www.w3.org/2001/vcard-rdf/3.0#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "id": { "@type": "@id", "@id": "@id" } + } +} \ No newline at end of file From d8bd7fb5297f663ad5898e21fb8019f8417a7b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 11:03:40 +0200 Subject: [PATCH 24/31] makes JSON-LD context for framing configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../resources/openapi/seed-xc-openapi.yaml | 12 +++++++++++ .../scdh/seed/xc/jena/SparqlConstruct.java | 21 +++++++++++++++---- .../seed/xc/jena/SparqlConstructTest.java | 18 +++++++++++----- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/api/src/main/resources/openapi/seed-xc-openapi.yaml b/api/src/main/resources/openapi/seed-xc-openapi.yaml index 9ba5799..2a957ad 100644 --- a/api/src/main/resources/openapi/seed-xc-openapi.yaml +++ b/api/src/main/resources/openapi/seed-xc-openapi.yaml @@ -417,6 +417,16 @@ components: type: type: string description: The local name of the parameter type. Write 'integer' instead of 'xs:integer'! + context: + description: Options of the JSON-LD context + type: object + required: + - location + properties: + location: + type: string + format: uri + description: The URI of the initial active context transformationInfo: description: Information about the transformation resource and its runtime parameters type: object @@ -490,6 +500,8 @@ components: $ref: '#/components/schemas/parser' serializer: $ref: '#/components/schemas/serializer' + context: + $ref: '#/components/schemas/context' required: - class - description diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 7d28216..4cc2293 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -11,6 +11,7 @@ import jakarta.json.*; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -33,8 +34,6 @@ public class SparqlConstruct implements Transformation { public static final String TRANSFORMATION_TYPE = "sparql-construct"; - String frame; - TransformationInfo transformationInfo; String query; @@ -73,6 +72,20 @@ public XsltParameterDetails getTransformationParameters() { return new XsltParameterDetails(); } + /** + * Returns the configured context location or null when there is none. + * @return - {@link URI} to the context location + */ + private URI getContextUri() { + URI result; + if (transformationInfo.getContext() == null) { + result = null; + } else { + result = transformationInfo.getContext().getLocation(); + } + return result; + } + @Override public byte[] transform( RuntimeParameters parameters, @@ -110,7 +123,7 @@ public byte[] transform( // write result back to the wire ByteArrayOutputStream output = new ByteArrayOutputStream(); RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); - if (!format.getLang().equals(Lang.JSONLD11) || frame == null) { + if (!format.getLang().equals(Lang.JSONLD11) || getContextUri() == null) { RDFDataMgr.write(output, resultModel, format); return output.toByteArray(); } else { @@ -119,7 +132,7 @@ public byte[] transform( DatasetGraph dsg = DatasetGraphFactory.create(resultModel.getGraph()); JsonArray ja = JenaToTitanium.convert(dsg, opts); JsonDocument jdoc = JsonDocument.of(ja); - JsonObject framed = JsonLd.frame(jdoc, frame).get(); + JsonObject framed = JsonLd.frame(jdoc, getContextUri()).get(); JsonWriter writer = Json.createWriter(output); writer.writeObject(framed); return output.toByteArray(); diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index cdd49cb..c25fa6f 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -37,6 +37,8 @@ class SparqlConstructTest { private static TransformationInfo QC1_JSONLD; + private static TransformationInfo QC1_JSONLD_WITH_CONTEXT; + @Inject HttpServerRequest request; @@ -77,6 +79,15 @@ private String getOutput() { QC1_JSONLD = info; } + static { + TransformationInfo info = new TransformationInfo(); + info.setPropertyClass(SparqlConstruct.TRANSFORMATION_TYPE); + info.setLocation(new File(RQ_DIR, "qc1.rq").getAbsolutePath()); + info.setMediaType("application/ld+json"); + info.setContext(new Context(FRAME.getAbsoluteFile().toURI())); + QC1_JSONLD_WITH_CONTEXT = info; + } + SparqlConstruct transformation; @BeforeEach @@ -124,7 +135,6 @@ public void testConstructQuerySerializeTurtle() transformation.setup(QC1_TTL, CONFIG); InputStream in = new FileInputStream(VCDB1); output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); - // assertEquals("", getOutput()); assertTrue(getOutput().startsWith("PREFIX rdf")); assertEquals(5, getOutput().lines().count()); } @@ -137,18 +147,16 @@ public void testConstructQuerySerializeJsonLd() InputStream in = new FileInputStream(VCDB1); output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); assertTrue(getOutput().contains("\"@id\": \"http://somewhere/JohnSmith\"")); - // assertEquals("", getOutput()); } @Test public void testConstructQuerySerializeFraming() throws ConfigurationException, TransformationPreparationException, TransformationException, FileNotFoundException { - transformation.setup(QC1_JSONLD, CONFIG); - transformation.frame = FRAME.getAbsoluteFile().toURI().toString(); + transformation.setup(QC1_JSONLD_WITH_CONTEXT, CONFIG); InputStream in = new FileInputStream(VCDB1); output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); assertTrue(getOutput().contains("\"id\":\"here:JohnSmith\"")); - // assertEquals("", getOutput()); + assertFalse(getOutput().contains("\"@id\":\"here:JohnSmith\"")); } } From 5911cd2d789aad9761f1ca68579335867eae5221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 12:16:18 +0200 Subject: [PATCH 25/31] encapsulates preparation of the JSON-LD framing context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../scdh/seed/xc/jena/SparqlConstruct.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 4cc2293..27a078c 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -3,6 +3,7 @@ import com.apicatalog.jsonld.JsonLd; import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.JsonLdOptions; +import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.document.JsonDocument; import de.ulbms.scdh.seed.xc.api.*; import io.smallrye.mutiny.Uni; @@ -86,6 +87,25 @@ private URI getContextUri() { return result; } + /** + * Returns the JSON-LD context as a {@link Document}. This method encapsulates + * the preparation of the context including all options, e.g. setting a timeout. + * @return the context {@link Document} + * @throws TransformationPreparationException when preparation failed + */ + private Document getContext() throws TransformationPreparationException { + try { + return JsonDocument.of(getContextUri().toURL().openStream()); + } catch (JsonLdError e) { + LOG.error("failed to read JSON-LD framing context {}", getContextUri()); + throw new TransformationPreparationException( + "failed to read JSON-LD framing context " + getContextUri(), e); + } catch (IOException | NullPointerException e) { + LOG.error("JSON-LD framing URI not found {}", getContextUri()); + throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); + } + } + @Override public byte[] transform( RuntimeParameters parameters, @@ -132,7 +152,7 @@ public byte[] transform( DatasetGraph dsg = DatasetGraphFactory.create(resultModel.getGraph()); JsonArray ja = JenaToTitanium.convert(dsg, opts); JsonDocument jdoc = JsonDocument.of(ja); - JsonObject framed = JsonLd.frame(jdoc, getContextUri()).get(); + JsonObject framed = JsonLd.frame(jdoc, getContext()).get(); JsonWriter writer = Json.createWriter(output); writer.writeObject(framed); return output.toByteArray(); @@ -144,7 +164,7 @@ public byte[] transform( LOG.error("failed to execute SPARQL query: {}", e.getMessage()); throw new TransformationException(e); } catch (JsonLdError e) { - LOG.error("failed to load into titanium json-ld, {}", e.getMessage()); + LOG.error("JSON-LD processing failed, {}", e.getMessage()); throw new TransformationException(e); } } From 9c624f54df52c4e4462be8a83a7b8b6ad3754ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 14:48:54 +0200 Subject: [PATCH 26/31] fixes resource management when getting context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../scdh/seed/xc/jena/SparqlConstruct.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 27a078c..b0d2085 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -12,7 +12,9 @@ import jakarta.json.*; import jakarta.ws.rs.InternalServerErrorException; import java.io.*; +import java.net.SocketTimeoutException; import java.net.URI; +import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -22,6 +24,7 @@ import org.apache.jena.riot.system.jsonld.JenaToTitanium; import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +38,12 @@ public class SparqlConstruct implements Transformation { public static final String TRANSFORMATION_TYPE = "sparql-construct"; + @ConfigProperty(name = "url-connect-timeout", defaultValue = "10000") + int contextConnectTimeout; + + @ConfigProperty(name = "url-read-timeout", defaultValue = "10000") + int contextReadTimeout; + TransformationInfo transformationInfo; String query; @@ -94,15 +103,32 @@ private URI getContextUri() { * @throws TransformationPreparationException when preparation failed */ private Document getContext() throws TransformationPreparationException { + InputStream in = null; try { - return JsonDocument.of(getContextUri().toURL().openStream()); + URLConnection connection = getContextUri().toURL().openConnection(); + connection.setConnectTimeout(contextConnectTimeout); + connection.setReadTimeout(contextReadTimeout); + in = connection.getInputStream(); + return JsonDocument.of(in); } catch (JsonLdError e) { LOG.error("failed to read JSON-LD framing context {}", getContextUri()); throw new TransformationPreparationException( "failed to read JSON-LD framing context " + getContextUri(), e); + } catch (SocketTimeoutException e) { + LOG.warn("timeout when reading JSON-LD context from {}", getContextUri()); + throw new TransformationPreparationException( + "timeout when reading JSON-LD context from " + getContextUri(), e); } catch (IOException | NullPointerException e) { LOG.error("JSON-LD framing URI not found {}", getContextUri()); throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } From dfa86ddf4e2e3aa6e501f2558ef8ac98a7cf7ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 14:53:24 +0200 Subject: [PATCH 27/31] make SPARQL plugin available in DTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- dts/pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dts/pom.xml b/dts/pom.xml index 794a769..cc7fbe7 100644 --- a/dts/pom.xml +++ b/dts/pom.xml @@ -33,12 +33,22 @@ seed-xc-transformations ${revision}${changelist} + ${project.groupId} seed-xc-saxon ${revision}${changelist} ${plugins.scope} + + ${project.groupId} + seed-xc-sparql + ${revision}${changelist} + ${plugins.scope} + ${project.groupId} seed-resource-providers From a322fbb145fbddb28b885d032016bd637d1b694c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 17:05:19 +0200 Subject: [PATCH 28/31] return after branching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index b0d2085..2f89d0d 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -171,7 +171,6 @@ public byte[] transform( RDFFormat format = serializer.getFormat(transformationInfo.getMediaType(), systemId, request); if (!format.getLang().equals(Lang.JSONLD11) || getContextUri() == null) { RDFDataMgr.write(output, resultModel, format); - return output.toByteArray(); } else { // use titanium for framing JsonLdOptions opts = new JsonLdOptions(); @@ -181,8 +180,8 @@ public byte[] transform( JsonObject framed = JsonLd.frame(jdoc, getContext()).get(); JsonWriter writer = Json.createWriter(output); writer.writeObject(framed); - return output.toByteArray(); } + return output.toByteArray(); } catch (RiotException e) { LOG.error("failed to read RDF dataset {}", e.getMessage()); throw new TransformationException(e); From 488b41e69321fc4b3d58e7fd457bc153763913ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 17:56:37 +0200 Subject: [PATCH 29/31] introduces size limit for JSON-LD context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../scdh/seed/xc/jena/SparqlConstruct.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java index 2f89d0d..180e370 100644 --- a/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java +++ b/plugins/seed-xc-sparql/src/main/java/de/ulbms/scdh/seed/xc/jena/SparqlConstruct.java @@ -44,6 +44,9 @@ public class SparqlConstruct implements Transformation { @ConfigProperty(name = "url-read-timeout", defaultValue = "10000") int contextReadTimeout; + @ConfigProperty(name = "context-max-size", defaultValue = "1048576") + long contextMaxSize; + TransformationInfo transformationInfo; String query; @@ -103,32 +106,29 @@ private URI getContextUri() { * @throws TransformationPreparationException when preparation failed */ private Document getContext() throws TransformationPreparationException { - InputStream in = null; + URLConnection connection; try { - URLConnection connection = getContextUri().toURL().openConnection(); + // toURL cannot not cause NPE because of way this method is used + connection = getContextUri().toURL().openConnection(); connection.setConnectTimeout(contextConnectTimeout); connection.setReadTimeout(contextReadTimeout); - in = connection.getInputStream(); + } catch (IOException | NullPointerException e) { + LOG.error("JSON-LD framing URI not found {}", getContextUri()); + throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); + } + try (InputStream in = connection.getInputStream()) { + if (contextMaxSize != 0 && connection.getContentLengthLong() > contextMaxSize) { + throw new TransformationPreparationException("context exceeds size limit"); + } return JsonDocument.of(in); - } catch (JsonLdError e) { - LOG.error("failed to read JSON-LD framing context {}", getContextUri()); - throw new TransformationPreparationException( - "failed to read JSON-LD framing context " + getContextUri(), e); } catch (SocketTimeoutException e) { LOG.warn("timeout when reading JSON-LD context from {}", getContextUri()); throw new TransformationPreparationException( "timeout when reading JSON-LD context from " + getContextUri(), e); - } catch (IOException | NullPointerException e) { - LOG.error("JSON-LD framing URI not found {}", getContextUri()); - throw new TransformationPreparationException("JSON-LD framing URI not found " + getContextUri(), e); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } + } catch (JsonLdError | IOException e) { + LOG.error("failed to read JSON-LD framing context {}", getContextUri()); + throw new TransformationPreparationException( + "failed to read JSON-LD framing context " + getContextUri(), e); } } From 744903149114c88a85d899675e9567fb5071689b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Apr 2026 17:56:58 +0200 Subject: [PATCH 30/31] about the plugin's application properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- plugins/seed-xc-sparql/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/seed-xc-sparql/README.md b/plugins/seed-xc-sparql/README.md index be5af95..8cd6608 100644 --- a/plugins/seed-xc-sparql/README.md +++ b/plugins/seed-xc-sparql/README.md @@ -1,6 +1,12 @@ # SPARQL Plugins -This module provides transformation plugins. +This module provides transformation plugins. It is based on +[Apache Jena](https://jena.apache.org/) and uses +[Titanium JSON-LD](https://github.com/filip26/titanium-json-ld) +for JSON-LD framing. + +For JSON-LD contexts used in framing, this plugin allows outbound +URL connections passing by the `ResourceProvider`. ## Transformation Types @@ -13,11 +19,17 @@ This module provides transformation plugins. - In case no content type is defined on the transformation level nor one is requested, the content tpye of input resource will be used - As a fallback, a default content type is used. -## Parameters +## Transformation Parameters Request parameters are passed to the query with a mechanism by [Apache Jena](https://jena.apache.org/documentation/query/parameterized-sparql-strings.html). It replaces all occurrences of a SPARQL variable `?X` with a literal. Parameter types should be declared with the parameter descripter as `xs:...` types. -`xs:string` is used as default. \ No newline at end of file +`xs:string` is used as default. + +## Application Properties + +- `url-connect-timeout`: time limit for establishing a connection to a remote JSON-LD context URL for framing, defaults to 10s +- `url-read-timeout`: time limit for fetching a remote JSON-LD context for framing, defaults to 10s +- `context-max-size`: size limit of JSON-LD context for framing, defaults to 1MB From 29e195ee90811f522ac2bd0f91a5f9a62c5ffa3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 27 Apr 2026 16:25:02 +0200 Subject: [PATCH 31/31] assert no resource leaks from input stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Lück --- .../seed/xc/jena/SparqlConstructTest.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java index c25fa6f..2d013c8 100644 --- a/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java +++ b/plugins/seed-xc-sparql/src/test/java/de/ulbms/scdh/seed/xc/jena/SparqlConstructTest.java @@ -5,10 +5,7 @@ import de.ulbms.scdh.seed.xc.api.*; import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; +import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import org.junit.jupiter.api.BeforeEach; @@ -128,6 +125,21 @@ public void testConstructQuery() assertEquals(1, getOutput().lines().count()); } + /* assert that are no resource leaks from input stream */ + @Test + public void testStreamClosed() + throws ConfigurationException, TransformationPreparationException, TransformationException, + FileNotFoundException, IOException { + transformation.setup(QC1, CONFIG); + BufferedInputStream in = new BufferedInputStream(new FileInputStream(VCDB1)); + in.mark(0); + in.reset(); + output = transformation.transform(null, null, VCDB1.getAbsolutePath(), in, null, request); + assertTrue(getOutput().startsWith("")); + assertEquals(1, getOutput().lines().count()); + assertThrows(IOException.class, () -> in.reset()); + } + @Test public void testConstructQuerySerializeTurtle() throws ConfigurationException, TransformationPreparationException, TransformationException,