diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh index 38ac459a..13c518fd 100644 --- a/.docker/entrypoint.sh +++ b/.docker/entrypoint.sh @@ -23,6 +23,8 @@ PARAMETER: input to be validated (only .ttl format supported) -shapesfile /data/myshapes.ttl [OPTIONAL] shapes for validation (only .ttl format supported) + -outputFormat ttl [OPTIONAL] - default is ttl + output format of the validation report or inferences graph, supported values: ttl, jelly -maxiterations 1 [OPTIONAL] - default is 1 iteratively applies the inference rules until the maximum number of iterations is reached (or no new triples are inferred) -validateShapes [OPTIONAL] diff --git a/.gitattributes b/.gitattributes index 0a23fec6..478999a1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,6 @@ # Force Unix bash files to use LF *.sh text eol=lf + +# Mark Jelly files as binary +*.jelly binary diff --git a/README.md b/README.md index 24d155d7..5129eb80 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,13 @@ COMMAND: to run rule inferencing PARAMETERS: -datafile /data/myfile.ttl [MANDATORY] - input to be validated (only .ttl format supported) + input to be validated (only .ttl and .jelly formats supported) -shapesfile /data/myshapes.ttl [OPTIONAL] - shapes for validation (only .ttl format supported) + shapes for validation (only .ttl and .jelly formats supported) -maxiterations 1 [OPTIONAL] - default is 1 iteratively applies the inference rules until the maximum number of iterations is reached (or no new triples are inferred) + -outputFormat ttl [OPTIONAL] - default is ttl + output format of the validation report or inferences graph, supported values: ttl, jelly -validateShapes [OPTIONAL] in case you want to include the metashapes (from the tosh namespace in particular) -addBlankNodes [OPTIONAL] @@ -161,6 +163,8 @@ After setting up the environment, you can run the command line utilities (i.e. v - Linux/Unix: `shaclvalidate.sh -datafile myfile.ttl -shapesfile myshapes.ttl` -Both tools (Windows, Linux) take the parameters described in the [Docker Usage](#docker-usage) section. **Currently, only Turtle (.ttl) files are supported.** +Both tools (Windows, Linux) take the parameters described in the [Docker Usage](#docker-usage) section. **Currently, only Turtle (.ttl) and [Jelly (.jelly)](https://w3id.org/jelly) files are supported.** + +To change the output format from Turtle to Jelly, you can use the `-outputFormat jelly` parameter. The tool print the validation report or the inferences graph to the output screen. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 27dbbb8f..260f3fb0 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,7 @@ UTF-8 5.2.0 + 3.6.2 4.13.2 2.0.17 2.24.3 @@ -83,6 +84,12 @@ ${ver.jena} + + eu.neverblink.jelly + jelly-jena + ${ver.jelly} + + junit junit diff --git a/src/main/java/org/topbraid/shacl/tools/AbstractTool.java b/src/main/java/org/topbraid/shacl/tools/AbstractTool.java index 02004c91..8c2e2e3c 100644 --- a/src/main/java/org/topbraid/shacl/tools/AbstractTool.java +++ b/src/main/java/org/topbraid/shacl/tools/AbstractTool.java @@ -16,6 +16,7 @@ */ package org.topbraid.shacl.tools; +import eu.neverblink.jelly.convert.jena.riot.JellyLanguage; import org.apache.jena.ontology.OntDocumentManager; import org.apache.jena.ontology.OntModel; import org.apache.jena.ontology.OntModelSpec; @@ -74,6 +75,25 @@ protected int getMaxIterations(String[] args) { return 1; } + protected String getOutputFormat(String[] args) { + for (int i = 0; i < args.length - 1; i++) { + if ("-outputFormat".equals(args[i])) { + switch (args[i + 1]) { + case "ttl": + return FileUtils.langTurtle; + case "jelly": + return JellyLanguage.JELLY.getName(); + default: + System.err.println("Unknown output format: " + args[i + 1] + + ". Supported formats: ttl, jelly"); + System.exit(1); + } + return args[i + 1]; + } + } + return FileUtils.langTurtle; // default output format + } + protected Model getDataModel(String[] args) throws IOException { for (int i = 0; i < args.length - 1; i++) { if (DATA_FILE.equals(args[i])) { @@ -98,7 +118,12 @@ private Model getModel(String[] args, int i) throws FileNotFoundException { String fileName = args[i + 1]; OntModel dataModel = ModelFactory.createOntologyModel(spec); File file = new File(fileName); - String lang = FileUtils.langTurtle; + String lang; + if (fileName.endsWith(".jelly")) { + lang = JellyLanguage.JELLY.getName(); + } else { + lang = FileUtils.langTurtle; + } dataModel.read(new FileInputStream(file), "urn:x:base", lang); return dataModel; } diff --git a/src/main/java/org/topbraid/shacl/tools/Infer.java b/src/main/java/org/topbraid/shacl/tools/Infer.java index ea8d0c08..b7b559c4 100644 --- a/src/main/java/org/topbraid/shacl/tools/Infer.java +++ b/src/main/java/org/topbraid/shacl/tools/Infer.java @@ -21,7 +21,6 @@ import java.io.PrintStream; import org.apache.jena.rdf.model.Model; -import org.apache.jena.util.FileUtils; import org.topbraid.shacl.rules.RuleUtil; /** @@ -48,6 +47,7 @@ public static void main(String[] args) throws IOException { private void run(String[] args) throws IOException { Model dataModel = getDataModel(args); Model shapesModel = getShapesModel(args); + String outFormat = getOutputFormat(args); if(shapesModel == null) { shapesModel = dataModel; } @@ -92,6 +92,6 @@ private void run(String[] args) throws IOException { } while (iteration < maxIterations); // print results - results.write(System.out, FileUtils.langTurtle); + results.write(System.out, outFormat); } } diff --git a/src/main/java/org/topbraid/shacl/tools/Validate.java b/src/main/java/org/topbraid/shacl/tools/Validate.java index b60e2250..c7940ba3 100644 --- a/src/main/java/org/topbraid/shacl/tools/Validate.java +++ b/src/main/java/org/topbraid/shacl/tools/Validate.java @@ -18,7 +18,6 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.util.FileUtils; import org.topbraid.jenax.util.JenaDatatypes; import org.topbraid.shacl.validation.ValidationUtil; import org.topbraid.shacl.vocabulary.SH; @@ -55,6 +54,7 @@ public static void main(String[] args) throws IOException { private void run(String[] args) throws IOException { Model dataModel = getDataModel(args); Model shapesModel = getShapesModel(args); + String outFormat = getOutputFormat(args); if (shapesModel == null) { shapesModel = dataModel; } @@ -72,7 +72,7 @@ private void run(String[] args) throws IOException { report.getModel().add(referencedNodes); } - report.getModel().write(System.out, FileUtils.langTurtle); + report.getModel().write(System.out, outFormat); if (report.hasProperty(SH.conforms, JenaDatatypes.FALSE)) { // See https://github.com/TopQuadrant/shacl/issues/56 diff --git a/src/test/java/org/topbraid/shacl/TestJelly.java b/src/test/java/org/topbraid/shacl/TestJelly.java new file mode 100644 index 00000000..90d46757 --- /dev/null +++ b/src/test/java/org/topbraid/shacl/TestJelly.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + */ +package org.topbraid.shacl; + +import eu.neverblink.jelly.convert.jena.riot.JellyLanguage; +import org.apache.jena.util.FileUtils; +import org.junit.Test; +import org.topbraid.jenax.util.JenaUtil; +import org.topbraid.shacl.tools.Infer; +import org.topbraid.shacl.tools.Validate; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * End-to-end tests for the CLI tools that use Jelly as the input and output format. + * + * @author Piotr SowiƄski + */ +public class TestJelly { + + @Test + public void testJellyInfer() throws Exception { + String dataFile = this.getClass().getResource("/jelly/infer-data.jelly").getFile(); + String ruleFile = this.getClass().getResource("/jelly/infer-rule.jelly").getFile(); + + // Redirect System.out to capture the output + var oldOut = System.out; + try { + // Run the rule with Turtle output format + var turtleOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(turtleOut)); + Infer.main(new String[]{ + "-datafile", dataFile, + "-shapesfile", ruleFile, + "-outputFormat", "ttl" + }); + + // And with Jelly output + var jellyOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(jellyOut)); + Infer.main(new String[]{ + "-datafile", dataFile, + "-shapesfile", ruleFile, + "-outputFormat", "jelly" + }); + + assert(turtleOut.size() > 0); + assert(jellyOut.size() > 0); + + // Check that the output contains the same number of triples + var turtleModel = JenaUtil.createDefaultModel().read( + new ByteArrayInputStream(turtleOut.toByteArray()), null, FileUtils.langTurtle); + var jellyModel = JenaUtil.createDefaultModel().read( + new ByteArrayInputStream(jellyOut.toByteArray()), null, JellyLanguage.JELLY.getName()); + assert(turtleModel.size() == jellyModel.size()); + } finally { + System.setOut(oldOut); + } + } + + @Test + public void testJellyValidate() throws Exception { + String dataFile = this.getClass().getResource("/jelly/validate-data.jelly").getFile(); + String shapeFile = this.getClass().getResource("/jelly/validate-shape.jelly").getFile(); + + // Redirect System.out to capture the output + var oldOut = System.out; + try { + // Run the validation with Turtle output format + var turtleOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(turtleOut)); + Validate.main(new String[]{ + "-datafile", dataFile, + "-shapesfile", shapeFile, + "-outputFormat", "ttl" + }); + + // Run the validation with Jelly output + var jellyOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(jellyOut)); + Validate.main(new String[]{ + "-datafile", dataFile, + "-shapesfile", shapeFile, + "-outputFormat", "jelly" + }); + + assert(turtleOut.size() > 0); + assert(jellyOut.size() > 0); + + // Check that the output contains the same number of triples + var turtleModel = JenaUtil.createDefaultModel().read( + new ByteArrayInputStream(turtleOut.toByteArray()), null, FileUtils.langTurtle); + var jellyModel = JenaUtil.createDefaultModel().read( + new ByteArrayInputStream(jellyOut.toByteArray()), null, JellyLanguage.JELLY.getName()); + assert(turtleModel.size() == jellyModel.size()); + } finally { + System.setOut(oldOut); + } + } +} diff --git a/src/test/resources/jelly/infer-data.jelly b/src/test/resources/jelly/infer-data.jelly new file mode 100644 index 00000000..d1b2ada4 Binary files /dev/null and b/src/test/resources/jelly/infer-data.jelly differ diff --git a/src/test/resources/jelly/infer-data.ttl b/src/test/resources/jelly/infer-data.ttl new file mode 100644 index 00000000..565d20f4 --- /dev/null +++ b/src/test/resources/jelly/infer-data.ttl @@ -0,0 +1,107 @@ +## Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 + +@prefix osh_kg: . +@prefix owl: . +@prefix qb: . +@prefix rdfs: . +@prefix sdmx-dimension: . +@prefix skos: . +@prefix xsd: . + +sdmx-dimension:refPeriod a qb:DimensionProperty, + owl:ObjectProperty ; + rdfs:range osh_kg:YearValue . + +osh_kg:year_2008 a osh_kg:YearValue ; + rdfs:label "2008"@en, + "2008"@pl ; + skos:notation "2008"^^xsd:gYear, + "2008" . + +osh_kg:year_2009 a osh_kg:YearValue ; + rdfs:label "2009"@en, + "2009"@pl ; + skos:notation "2009"^^xsd:gYear, + "2009" . + +osh_kg:year_2010 a osh_kg:YearValue ; + rdfs:label "2010"@en, + "2010"@pl ; + skos:notation "2010"^^xsd:gYear, + "2010" . + +osh_kg:year_2011 a osh_kg:YearValue ; + rdfs:label "2011"@en, + "2011"@pl ; + skos:notation "2011"^^xsd:gYear, + "2011" . + +osh_kg:year_2012 a osh_kg:YearValue ; + rdfs:label "2012"@en, + "2012"@pl ; + skos:notation "2012"^^xsd:gYear, + "2012" . + +osh_kg:year_2013 a osh_kg:YearValue ; + rdfs:label "2013"@en, + "2013"@pl ; + skos:notation "2013"^^xsd:gYear, + "2013" . + +osh_kg:year_2014 a osh_kg:YearValue ; + rdfs:label "2014"@en, + "2014"@pl ; + skos:notation "2014"^^xsd:gYear, + "2014" . + +osh_kg:year_2015 a osh_kg:YearValue ; + rdfs:label "2015"@en, + "2015"@pl ; + skos:notation "2015"^^xsd:gYear, + "2015" . + +osh_kg:year_2016 a osh_kg:YearValue ; + rdfs:label "2016"@en, + "2016"@pl ; + skos:notation "2016"^^xsd:gYear, + "2016" . + +osh_kg:year_2017 a osh_kg:YearValue ; + rdfs:label "2017"@en, + "2017"@pl ; + skos:notation "2017"^^xsd:gYear, + "2017" . + +osh_kg:year_2018 a osh_kg:YearValue ; + rdfs:label "2018"@en, + "2018"@pl ; + skos:notation "2018"^^xsd:gYear, + "2018" . + +osh_kg:year_2019 a osh_kg:YearValue ; + rdfs:label "2019"@en, + "2019"@pl ; + skos:notation "2019"^^xsd:gYear, + "2019" . + +osh_kg:year_2020 a osh_kg:YearValue ; + rdfs:label "2020"@en, + "2020"@pl ; + skos:notation "2020"^^xsd:gYear, + "2020" . + +osh_kg:year_2021 a osh_kg:YearValue ; + rdfs:label "2021"@en, + "2021"@pl ; + skos:notation "2021"^^xsd:gYear, + "2021" . + +osh_kg:year_2022 a osh_kg:YearValue ; + rdfs:label "2022"@en, + "2022"@pl ; + skos:notation "2022"^^xsd:gYear, + "2022" . + +osh_kg:YearValue a owl:Class ; + rdfs:subClassOf skos:Concept . + diff --git a/src/test/resources/jelly/infer-rule.jelly b/src/test/resources/jelly/infer-rule.jelly new file mode 100644 index 00000000..df50dbf7 Binary files /dev/null and b/src/test/resources/jelly/infer-rule.jelly differ diff --git a/src/test/resources/jelly/infer-rule.ttl b/src/test/resources/jelly/infer-rule.ttl new file mode 100644 index 00000000..a4f81feb --- /dev/null +++ b/src/test/resources/jelly/infer-rule.ttl @@ -0,0 +1,32 @@ +## Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 + +PREFIX osh_kg: +PREFIX rdf: +PREFIX sh: +PREFIX skos: +PREFIX temporal: + +osh_kg:YearValueRule a sh:NodeShape ; + sh:targetClass osh_kg:YearValue ; + sh:rule [ + a sh:SPARQLRule ; + sh:construct """ + PREFIX skos: + PREFIX osh_kg: + PREFIX temporal: + PREFIX xsd: + + CONSTRUCT { + $this temporal:precedes ?next . + ?next temporal:follows $this . + } + WHERE { + $this skos:notation ?year . + FILTER(DATATYPE(?year) = xsd:gYear) . + BIND(xsd:gYear(str(xsd:int(?year) + 1)) AS ?nextYear) . + + ?next a osh_kg:YearValue ; + skos:notation ?nextYear . + } + """ ; + ] . diff --git a/src/test/resources/jelly/validate-data.jelly b/src/test/resources/jelly/validate-data.jelly new file mode 100644 index 00000000..ff6307f1 Binary files /dev/null and b/src/test/resources/jelly/validate-data.jelly differ diff --git a/src/test/resources/jelly/validate-data.ttl b/src/test/resources/jelly/validate-data.ttl new file mode 100644 index 00000000..17b0cc01 --- /dev/null +++ b/src/test/resources/jelly/validate-data.ttl @@ -0,0 +1,17 @@ +## Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 + +@prefix : . +@prefix dcterms: . +@prefix foaf: . +@prefix rb: . + +# This file contains the manually written metadata for the benchmark category. +# The URI of the category here is temporary. Real URIs are assigned automatically in CI. + +:category + a rb:Category ; + dcterms:conformsTo ; + dcterms:identifier "flat" ; + dcterms:title "Flat RDF (sequences of triples/quads)"@en ; + dcterms:description """Benchmark category of generic tasks involving flat RDF streams (elements are either RDF triples or RDF quads). Each dataset in this category can be also treated as a single RDF graph/RDF dataset."""@en ; +. diff --git a/src/test/resources/jelly/validate-shape.jelly b/src/test/resources/jelly/validate-shape.jelly new file mode 100644 index 00000000..44eedb1d Binary files /dev/null and b/src/test/resources/jelly/validate-shape.jelly differ diff --git a/src/test/resources/jelly/validate-shape.ttl b/src/test/resources/jelly/validate-shape.ttl new file mode 100644 index 00000000..90a60089 --- /dev/null +++ b/src/test/resources/jelly/validate-shape.ttl @@ -0,0 +1,51 @@ +## Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 + +PREFIX : +PREFIX dcat: +PREFIX dcterms: +PREFIX foaf: +PREFIX rb: +PREFIX rdf: +PREFIX rdfs: +PREFIX schema: +PREFIX sh: +PREFIX skos: +PREFIX spdx: +PREFIX stax: +PREFIX void: +PREFIX xsd: + +:CategoryShape rdf:type sh:NodeShape; + sh:property [ sh:datatype rdf:langString; + sh:minCount 1; + sh:path dcterms:description; + sh:uniqueLang true + ]; + sh:property [ sh:datatype rdf:langString; + sh:minCount 1; + sh:path dcterms:title; + sh:uniqueLang true + ]; + sh:property [ sh:datatype xsd:string; + sh:maxCount 1; + sh:minCount 1; + sh:path dcterms:identifier + ]; + sh:property [ sh:hasValue ; + sh:maxCount 1; + sh:minCount 1; + sh:path dcterms:conformsTo + ]; + sh:property [ sh:hasValue rb:Category; + sh:maxCount 1; + sh:minCount 1; + sh:path rdf:type + ]; + sh:targetNode . + +:CategoryGraphShape rdf:type sh:NodeShape; + sh:property [ sh:maxCount 1; + sh:minCount 1; + sh:path [ sh:inversePath rdf:type ] + ]; + sh:targetNode rb:Category .