Merge pull request #752 from afs/shacl-dev
JENA-1905, JENA-1906, JENA-1907: SHACL improvements
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java b/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java
index 286b740..60d9d4a 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java
@@ -62,7 +62,9 @@
public static final String paramQueryRef = "query-ref" ;
public static final String paramDefaultGraphURI = "default-graph-uri" ;
public static final String paramNamedGraphURI = "named-graph-uri" ;
-
+ public static final String paramTarget = "target" ;
+
+
public static final String paramStyleSheet = "stylesheet" ;
public static final String paramAccept = "accept" ;
public static final String paramOutput1 = "output" ; // See Yahoo! developer: http://developer.yahoo.net/common/json.html
diff --git a/jena-cmds/pom.xml b/jena-cmds/pom.xml
index 611ff2c..b528dbe 100644
--- a/jena-cmds/pom.xml
+++ b/jena-cmds/pom.xml
@@ -81,6 +81,13 @@
<classifier>tests</classifier>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>org.apache.jena</groupId>
+ <artifactId>jena-shacl</artifactId>
+ <version>3.16.0-SNAPSHOT</version>
+ <classifier>tests</classifier>
+ <optional>true</optional>
+ </dependency>
<!-- Command Logging -->
<dependency>
diff --git a/jena-cmds/src/main/java/shacl/shacl_parse.java b/jena-cmds/src/main/java/shacl/shacl_parse.java
index 52c032c..14718c1 100644
--- a/jena-cmds/src/main/java/shacl/shacl_parse.java
+++ b/jena-cmds/src/main/java/shacl/shacl_parse.java
@@ -22,10 +22,11 @@
import org.apache.jena.atlas.logging.LogCtl;
import org.apache.jena.shacl.Shapes;
import org.apache.jena.shacl.lib.ShLib;
+import org.apache.jena.shacl.parser.ShaclParseException;
import org.apache.jena.sys.JenaSystem;
/** SHACL parsing.
- * <p>
+ * <p>
* Usage: <code>shacl parse FILE</code>
*/
public class shacl_parse extends CmdGeneral {
@@ -65,16 +66,39 @@
@Override
protected void exec() {
+ boolean multipleFiles = (positionals.size() > 1) ;
positionals.forEach(fn->{
- exec(fn);
+ exec(fn, multipleFiles);
});
}
- private void exec(String fn) {
- Shapes shapes = Shapes.parse(fn);
+ private void exec(String fn, boolean multipleFiles) {
+ Shapes shapes;
+ try {
+ shapes = Shapes.parseAll(fn);
+ } catch (ShaclParseException ex) {
+ if ( multipleFiles )
+ System.err.println(fn+" : ");
+ System.err.println(ex.getMessage());
+ return;
+ }
ShLib.printShapes(shapes);
int numShapes = shapes.numShapes();
int numRootShapes = shapes.numRootShapes();
- System.out.printf("Shapes = %,d : Root shapes = %,d\n", numShapes, numRootShapes);
+ if ( isVerbose() ) {
+ System.out.println();
+ System.out.println("Target shapes: ");
+ shapes.getShapeMap().forEach((n,shape)->{
+ if ( shape.hasTarget() )
+ System.out.println(" "+ShLib.displayStr(shape.getShapeNode()));
+ });
+
+ System.out.println("Other Shapes: ");
+ shapes.getShapeMap().forEach((n,shape)->{
+ if ( ! shape.hasTarget() )
+ System.out.println(" "+ShLib.displayStr(shape.getShapeNode()));
+ });
+ }
+
}
}
diff --git a/jena-cmds/src/main/java/shacl/shacl_validate.java b/jena-cmds/src/main/java/shacl/shacl_validate.java
index d2e5bc7..901cb4f 100644
--- a/jena-cmds/src/main/java/shacl/shacl_validate.java
+++ b/jena-cmds/src/main/java/shacl/shacl_validate.java
@@ -23,8 +23,11 @@
import jena.cmd.CmdGeneral;
import org.apache.jena.atlas.logging.LogCtl;
import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RiotException;
import org.apache.jena.shacl.ValidationReport;
import org.apache.jena.shacl.lib.ShLib;
import org.apache.jena.shacl.validation.ValidationProc;
@@ -44,10 +47,12 @@
private ArgDecl argOutputText = new ArgDecl(false, "--text");
//private ArgDecl argOutputRDF = new ArgDecl(false, "--rdf");
private ArgDecl argData = new ArgDecl(true, "--data", "--datafile", "-d");
- private ArgDecl argShapes = new ArgDecl(true, "--shapes", "--shapesfile", "-s");
+ private ArgDecl argShapes = new ArgDecl(true, "--shapes", "--shapesfile", "--shapefile", "-s");
+ private ArgDecl argTargetNode = new ArgDecl(true, "--target", "--node", "-n");
- private String datafile = null;
- private String shapesfile = null;
+ private String datafile = null;
+ private String shapesfile = null;
+ private String targetNode = null; // Parse later.
private boolean textOutput = false;
public static void main (String... argv) {
@@ -56,16 +61,16 @@
public shacl_validate(String[] argv) {
super(argv) ;
- // Includes -datafile myfile.ttl -shapesfile myshapes.ttl
super.add(argShapes, "--shapes", "Shapes file");
super.add(argData, "--data", "Data file");
+ super.add(argTargetNode, "--target", "Validate specific node [may use prefixes from the data]");
super.add(argOutputText, "--text", "Output in concise text format");
//super.add(argOutputRDF, "--rdf", "Output in RDF (Turtle) format");
}
@Override
protected String getSummary() {
- return getCommandName()+" --shapes shapesFile --data dataFile";
+ return getCommandName()+" [--target URI] --shapes shapesFile --data dataFile";
}
@Override
@@ -87,25 +92,49 @@
throw new CmdException("Usage: "+getSummary());
if ( shapesfile == null )
shapesfile = datafile;
-
+
textOutput = super.hasArg(argOutputText);
+
+ if ( contains(argTargetNode) ) {
+ targetNode = getValue(argTargetNode);
+ }
}
@Override
protected void exec() {
- Graph shapesGraph = RDFDataMgr.loadGraph(shapesfile);
+ Graph shapesGraph = load(shapesfile, "shapes file");
Graph dataGraph;
if ( datafile.equals(shapesfile) )
dataGraph = shapesGraph;
else
- dataGraph = RDFDataMgr.loadGraph(datafile);
- ValidationReport report = ValidationProc.simpleValidation(shapesGraph, dataGraph, isVerbose());
+ dataGraph = load(datafile, "data file");
+
+ Node node = null;
+ if ( targetNode != null ) {
+ String x = dataGraph.getPrefixMapping().expandPrefix(targetNode);
+ node = NodeFactory.createURI(x);
+ }
+
+ ValidationReport report = ( node != null )
+ ? ValidationProc.simpleValidation(shapesGraph, dataGraph, node, isVerbose())
+ : ValidationProc.simpleValidation(shapesGraph, dataGraph, isVerbose());
+
if ( textOutput )
ShLib.printReport(report);
else
RDFDataMgr.write(System.out, report.getGraph(), Lang.TTL);
}
+ private Graph load(String filename, String scope) {
+ try {
+ Graph graph = RDFDataMgr.loadGraph(filename);
+ return graph;
+ } catch (RiotException ex) {
+ System.err.println("Loading "+scope);
+ throw ex;
+ }
+ }
+
@Override
protected String getCommandName() {
return "shacl_validate";
diff --git a/jena-cmds/src/test/java/shacl/shacl_test.java b/jena-cmds/src/test/java/shacl/shacl_test.java
new file mode 100644
index 0000000..35585f7
--- /dev/null
+++ b/jena-cmds/src/test/java/shacl/shacl_test.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package shacl;
+
+import jena.cmd.CmdGeneral;
+import org.apache.jena.atlas.logging.Log;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.shacl.testing.RunManifest;
+import org.apache.jena.sys.JenaSystem;
+
+public class shacl_test extends CmdGeneral {
+
+ static {
+ LogCtl.setCmdLogging();
+ JenaSystem.init();
+ }
+
+ public shacl_test(String[] argv) {
+ super(argv);
+ }
+
+ public static void main (String... argv) {
+ new shacl_test(argv).mainRun() ;
+ }
+
+ @Override
+ protected String getSummary() {
+ return getCommandName()+" FILE";
+ }
+
+ @Override
+ protected void exec() {
+ if ( getPositional().isEmpty() ) {
+ Log.warn(this, "No manifests");
+ }
+
+ for ( String fn : getPositional() ) {
+ RunManifest.runTest(fn, isVerbose());
+ }
+ }
+
+ @Override
+ protected String getCommandName() {
+ return "shacl_test";
+ }
+}
+
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
index 3fa1a2e..f70aeab 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
@@ -24,8 +24,11 @@
import org.apache.jena.atlas.web.MediaType;
import org.apache.jena.fuseki.DEF;
import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.riot.web.HttpNames;
import org.apache.jena.shacl.ShaclValidator;
import org.apache.jena.shacl.Shapes;
import org.apache.jena.shacl.ValidationReport;
@@ -34,8 +37,11 @@
/**
* SHACL validation service. Receives a shapes file and validates a graph named in the
* {@code ?graph=} parameter.
+ * <p>
* {@code ?graph=} can be any graph name, or one of the words "default" or "union" (without quotes)
* to indicate the default graph, which is also the default and the dataset union graph.
+ * <p>
+ * Optional parameter {@code ?target=} specifies the target node for the validation report.
*/
public class SHACL_Validation extends BaseActionREST { //ActionREST {
@@ -48,16 +54,28 @@
Lang lang = RDFLanguages.contentTypeToLang(mediaType.getContentType());
if ( lang == null )
lang = RDFLanguages.TTL;
+
+ String targetNodeStr = action.getRequest().getParameter(HttpNames.paramTarget);
action.beginRead();
try {
- GraphTarget target = determineTarget(action.getActiveDSG(), action);
- if ( ! target.exists() )
- ServletOps.errorNotFound("No data graph: "+target.label());
- Graph data = target.graph();
+ GraphTarget graphTarget = determineTarget(action.getActiveDSG(), action);
+ if ( ! graphTarget.exists() )
+ ServletOps.errorNotFound("No data graph: "+graphTarget.label());
+ Graph data = graphTarget.graph();
Graph shapesGraph = ActionLib.readFromRequest(action, Lang.TTL);
+
+ Node targetNode = null;
+ if ( targetNodeStr != null ) {
+ String x = data.getPrefixMapping().expandPrefix(targetNodeStr);
+ targetNode = NodeFactory.createURI(x);
+ }
+
Shapes shapes = Shapes.parse(shapesGraph);
- ValidationReport report = ShaclValidator.get().validate(shapesGraph, data);
+ ValidationReport report = ( targetNode == null )
+ ? ShaclValidator.get().validate(shapesGraph, data)
+ : ShaclValidator.get().validate(shapesGraph, data, targetNode);
+
if ( report.conforms() )
action.log.info(format("[%d] shacl: conforms", action.id));
else
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
index cc73682..b800dea 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
@@ -77,7 +77,7 @@
conn.put(DIR+"data1.ttl");
ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=default", DIR+"shapes1.ttl");
assertNotNull(report);
- assertEquals(2, report.getEntries().size());
+ assertEquals(3, report.getEntries().size());
conn.update("CLEAR ALL");
}
}
@@ -114,7 +114,7 @@
conn.put("urn:abc:graph", DIR+"data1.ttl");
ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=union", DIR+"shapes1.ttl");
assertNotNull(report);
- assertEquals(2, report.getEntries().size());
+ assertEquals(3, report.getEntries().size());
conn.update("CLEAR ALL");
}
}
@@ -125,11 +125,44 @@
conn.put("urn:abc:graph", DIR+"data1.ttl");
ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=urn:abc:graph", DIR+"shapes1.ttl");
assertNotNull(report);
+ assertEquals(3, report.getEntries().size());
+ conn.update("CLEAR ALL");
+ }
+ }
+
+ @Test
+ public void shacl_targetNode_1() {
+ try ( RDFConnection conn = RDFConnectionFactory.connect(serverURL+"/ds")) {
+ conn.put("urn:abc:graph", DIR+"data1.ttl");
+ ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=urn:abc:graph&target=:s1", DIR+"shapes1.ttl");
+ assertNotNull(report);
assertEquals(2, report.getEntries().size());
conn.update("CLEAR ALL");
}
}
+ @Test
+ public void shacl_targetNode_2() {
+ try ( RDFConnection conn = RDFConnectionFactory.connect(serverURL+"/ds")) {
+ conn.put("urn:abc:graph", DIR+"data1.ttl");
+ ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=urn:abc:graph&target=:s3", DIR+"shapes1.ttl");
+ assertNotNull(report);
+ assertEquals(0, report.getEntries().size());
+ conn.update("CLEAR ALL");
+ }
+ }
+
+ @Test
+ public void shacl_targetNode_3() {
+ try ( RDFConnection conn = RDFConnectionFactory.connect(serverURL+"/ds")) {
+ conn.put("urn:abc:graph", DIR+"data1.ttl");
+ ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=urn:abc:graph&target=http://nosuch/node/", DIR+"shapes1.ttl");
+ assertNotNull(report);
+ assertEquals(0, report.getEntries().size());
+ conn.update("CLEAR ALL");
+ }
+ }
+
private static ValidationReport validateReport(String url, String shapesFile) {
Graph shapesGraph = RDFDataMgr.loadGraph(shapesFile);
EntityTemplate entity = new EntityTemplate((out)->RDFDataMgr.write(out, shapesGraph, Lang.TTL));
diff --git a/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/data1.ttl b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/data1.ttl
index da5ad92..64a85d3 100644
--- a/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/data1.ttl
+++ b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/data1.ttl
@@ -12,5 +12,8 @@
ns:p 60 ;
.
+## Invalid
+:s2 ns:p 57 .
+
# Valid.
-:s2 ns:p "a string";.
+:s3 ns:p "a string";.
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/ShaclValidator.java b/jena-shacl/src/main/java/org/apache/jena/shacl/ShaclValidator.java
index 810f232..4dfa5d0 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/ShaclValidator.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/ShaclValidator.java
@@ -69,4 +69,9 @@
public default ValidationReport validate(Graph shapesGraph, Graph data) {
return validate(parse(shapesGraph), data);
}
+
+ /** Produce a node-specific validation report. */
+ public default ValidationReport validate(Graph shapesGraph, Graph data, Node target) {
+ return validate(parse(shapesGraph), data, target);
+ }
}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/SparqlConstraints.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/SparqlConstraints.java
index 1b8e17b..0e38bee 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/SparqlConstraints.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/SparqlConstraints.java
@@ -85,7 +85,8 @@
String qs = prefixes+"\n"+selectQuery;
try {
Query query = QueryFactory.create(qs);
- return new SparqlConstraint(query);
+ String msg = (message != null && message.isLiteral() ? message.getLiteralLexicalForm() : null );
+ return new SparqlConstraint(query, msg);
} catch (QueryParseException ex) {
throw new ShaclParseException("SPARQL parse error: "+ex.getMessage()+"\n"+qs);
}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/Targets.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/Targets.java
index d880a83..de563f0 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/Targets.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/Targets.java
@@ -41,7 +41,6 @@
2.1.3.5 Objects-of targets (sh:targetObjectsOf)
*/
-
public Graph shapesGraph;
public Set<Node> targetNodes;
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintComponentSPARQL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintComponentSPARQL.java
index f5d61f4..a6221d9 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintComponentSPARQL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintComponentSPARQL.java
@@ -36,14 +36,13 @@
/** SPARQL Constraint (ASK or SELECT) */
public class ConstraintComponentSPARQL implements Constraint {
- // XXX Rename if we do not have subclasses.
- // XXX Need rule "if nodeShape, nodeValidator then validator". "if propertyShape, propertyValidator then validator."
-
protected final SparqlComponent sparqlConstraintComponent;
protected final Multimap<Parameter, Node> parameterMap;
protected final Query query;
- public ConstraintComponentSPARQL(SparqlComponent sparqlConstraintComponent, Multimap<Parameter, Node> parameterMap) {
+ public ConstraintComponentSPARQL(SparqlComponent sparqlConstraintComponent,
+ Multimap<Parameter, Node> parameterMap) {
+ //sh:labelTemplate
this.sparqlConstraintComponent = sparqlConstraintComponent;
this.parameterMap = parameterMap;
@@ -60,13 +59,16 @@
@Override
public void validateNodeShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
SparqlValidation.validate(vCxt, data, shape, focusNode, null, focusNode, query, parameterMap,
- new ReportConstraint(sparqlConstraintComponent.getReportComponent()));
+ sparqlConstraintComponent.getMessage(),
+ new ReportConstraint(sparqlConstraintComponent.getReportComponent()));
}
@Override
public void validatePropertyShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode, Path path, Set<Node> valueNodes) {
- valueNodes.forEach(vn->SparqlValidation.validate(vCxt, data, shape, focusNode, path, vn, query, parameterMap,
- new ReportConstraint(sparqlConstraintComponent.getReportComponent())));
+ valueNodes.forEach(vn->
+ SparqlValidation.validate(vCxt, data, shape, focusNode, path, vn, query, parameterMap,
+ sparqlConstraintComponent.getMessage(),
+ new ReportConstraint(sparqlConstraintComponent.getReportComponent())));
}
@Override
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlComponent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlComponent.java
index 3124842..78d13da 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlComponent.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlComponent.java
@@ -33,12 +33,14 @@
private final List<Parameter> params;
private final List<Node> requiredParameters;
private final List<Node> optionalParameters;
+ private final String message;
- public SparqlComponent(Node reportNode, boolean isSelect, String sparqlString, List<Parameter> params) {
+ public SparqlComponent(Node reportNode, boolean isSelect, String sparqlString, List<Parameter> params, String message) {
this.reportNode = reportNode;
this.sparqlString = sparqlString;
this.isSelect = isSelect;
this.params = params;
+ this.message = message;
this.requiredParameters = params.stream()
.filter(param->!param.isOptional())
.map(param->param.getParameterPath())
@@ -65,6 +67,10 @@
return params;
}
+ public String getMessage() {
+ return message;
+ }
+
public List<Node> getRequiredParameters() {
return requiredParameters;
}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlConstraint.java
index 45b67b3..f00bb60 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlConstraint.java
@@ -31,28 +31,31 @@
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.path.Path;
+/** SPARQL Constraint (ASK or SELECT) */
public class SparqlConstraint implements Constraint {
private final Query query;
+ private String message;
static Var varValue = Var.alloc("value");
// Output
static Var varPath = Var.alloc("path");
// Input substitution.
static Var varPATH = Var.alloc("PATH");
- public SparqlConstraint(Query query) {
+ public SparqlConstraint(Query query, String message) {
this.query = query;
+ this.message = message;
}
@Override
public void validateNodeShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
- SparqlValidation.validate(vCxt, data, shape, focusNode, null, focusNode, query, null, this);
+ SparqlValidation.validate(vCxt, data, shape, focusNode, null, focusNode, query, null, message, this);
}
@Override
public void validatePropertyShape(ValidationContext vCxt, Graph data, Shape shape,
Node focusNode, Path path, Set<Node> valueNodes) {
- valueNodes.forEach(vn->SparqlValidation.validate(vCxt, data, shape, focusNode, path, vn, query, null, this));
+ valueNodes.forEach(vn->SparqlValidation.validate(vCxt, data, shape, focusNode, path, vn, query, null, message, this));
}
@Override
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlValidation.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlValidation.java
index 60cf2eb..c534c73 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlValidation.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/SparqlValidation.java
@@ -18,14 +18,12 @@
package org.apache.jena.shacl.engine.constraint;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.jena.atlas.logging.Log;
import org.apache.jena.ext.com.google.common.collect.Multimap;
import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Node;
@@ -63,8 +61,11 @@
public static void validate(ValidationContext vCxt, Graph data, Shape shape,
Node focusNode, Path path, Node valueNode,
Query query, Multimap<Parameter, Node> parameterMap,
- Constraint reportConstraint) {
- // Two subcases:
+ String violationTemplate, Constraint reportConstraint) {
+ // Two sub-cases:
+ // Syntax rule: https://www.w3.org/TR/shacl/#syntax-rule-multiple-parameters
+ // If there are >1 parameters, each must be single valued.
+ // so:
// Multimap, one parameter, multiple values => conjunction of each, with one report.
// Multimap, any number of parameters, single values => single validation with one report.
@@ -72,7 +73,7 @@
if ( parameterMap.keySet().size() == 1 && parameterMap.size() > 1 ) {
for ( Entry<Parameter, Node> e : parameterMap.entries()) {
Map<Parameter, Node> pmap = Collections.singletonMap(e.getKey(), e.getValue());
- boolean b = validateMap(vCxt, data, shape, focusNode, path, valueNode, query, pmap, reportConstraint);
+ boolean b = validateMap(vCxt, data, shape, focusNode, path, valueNode, query, pmap, violationTemplate, reportConstraint);
if ( ! b )
// Validation error - return early.
return;
@@ -83,7 +84,7 @@
// Convert to map.
Map<Parameter, Node> pmap = flatten(parameterMap);
/*boolean b =*/
- validateMap(vCxt, data, shape, focusNode, path, valueNode, query, pmap, reportConstraint);
+ validateMap(vCxt, data, shape, focusNode, path, valueNode, query, pmap, violationTemplate, reportConstraint);
}
private static Map<Parameter, Node> flatten(Multimap<Parameter, Node> parameterMap) {
@@ -100,7 +101,7 @@
private static boolean validateMap(ValidationContext vCxt, Graph data, Shape shape,
Node focusNode, Path path, Node valueNode,
Query _query, Map<Parameter, Node> parameterMap,
- Constraint reportConstraint) {
+ String violationTemplate, Constraint reportConstraint) {
Model model = ModelFactory.createModelForGraph(data);
QueryExecution qExec;
@@ -128,19 +129,15 @@
if ( qExec.getQuery().isAskType() ) {
boolean b = qExec.execAsk();
if ( ! b ) {
- String msg = "SPARQL ASK constraint for "+ShLib.displayStr(valueNode)+" returns false";
+ String msg = ( violationTemplate == null )
+ ? "SPARQL ASK constraint for "+ShLib.displayStr(valueNode)+" returns false"
+ : substitute(violationTemplate, parameterMap, focusNode, path, valueNode);
vCxt.reportEntry(msg, shape, focusNode, path, valueNode, reportConstraint);
}
return b;
}
ResultSet rs = qExec.execSelect();
-// if ( true ) { // Development
-// ResultSetRewindable rsw = ResultSetFactory.makeRewindable(rs);
-// ResultSetFormatter.out(rsw);
-// rsw.reset();
-// rs = rsw;
-// }
if ( ! rs.hasNext() )
return true;
@@ -151,10 +148,15 @@
value = valueNode;
String msg;
- if ( value != null )
- msg = "SPARQL SELECT constraint for "+ShLib.displayStr(valueNode)+" returns "+ShLib.displayStr(value);
- else
- msg = "SPARQL SELECT constraint for "+ShLib.displayStr(valueNode)+" returns row "+row;
+ if ( violationTemplate == null ) {
+ if ( value != null )
+ msg = "SPARQL SELECT constraint for "+ShLib.displayStr(valueNode)+" returns "+ShLib.displayStr(value);
+ else
+ msg = "SPARQL SELECT constraint for "+ShLib.displayStr(valueNode)+" returns row "+row;
+ } else {
+ msg = substitute(violationTemplate, row);
+ }
+
Path rPath = path;
if ( rPath == null ) {
Node qPath = row.get(SparqlConstraint.varPath);
@@ -166,6 +168,47 @@
return false;
}
+ /** Result message: SELECT substitute */
+ private static String substitute(String violationTemplate, Binding row) {
+ String x = violationTemplate;
+ Iterator<Var> iter = row.vars();
+ while(iter.hasNext()) {
+ Var var = iter.next();
+ x = substit(x, var.getVarName(), row.get(var));
+ }
+ return x;
+ }
+
+ /** Result message: ASK substitute */
+ private static String substitute(String violationTemplate, Map<Parameter, Node> parameterMap, Node focusNode, Path path, Node valueNode) {
+ String x = violationTemplate;
+ for ( Entry<Parameter, Node> e : parameterMap.entrySet() ) {
+ x = substit(x, e.getKey().getSparqlName(), e.getValue());
+ }
+ return x;
+ }
+
+ /** Substitution */
+ private static String substit(String x, String name, Node value) {
+ try {
+ String vn = "\\{[?$]"+Matcher.quoteReplacement(name)+"\\}";
+ String val = strQuoted(value);
+ return x.replaceAll(vn, val);
+ } catch (RuntimeException ex) {
+ Log.warn(SparqlValidation.class, "Failed to substitute into string for name="+name+" value="+value);
+ return x;
+ }
+ }
+
+ /** regex-safe string */
+ private static String strQuoted(Node node) {
+ String x =
+ node.isLiteral() ?node.getLiteralLexicalForm()
+ : NodeFmtLib.str(node);
+ x = Matcher.quoteReplacement(x);
+ return x;
+ }
+
private static Map<Var, Node> parameterMapToSyntaxSubstitutions(Map<Parameter, Node> parameterMap, Node thisNode, Path path) {
Map<Var, Node> substitions = parametersToMap(parameterMap, thisNode);
if ( path != null ) {
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/G.java b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/G.java
index b0e4f8e..bafc174 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/G.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/G.java
@@ -30,14 +30,8 @@
import org.apache.jena.sparql.util.graph.GraphList;
import org.apache.jena.util.iterator.ExtendedIterator;
-/** Library of functions for convenience wokring direction with Graph and Node. */
+/** Library of functions for convenience working directly with Graph and Node. */
public class G {
- // Node filter tests.
-// public static boolean isURI(GNode n) { return n != null && isURI(n.getNode()); }
-// public static boolean isBlank(GNode n) { return n != null && isBlank(n.getNode()); }
-// public static boolean isLiteral(GNode n) { return n != null && isLiteral(n.getNode()); }
-// public static boolean isResource(GNode n) { return n != null && isURI(n.getNode())||isBlank(n.getNode()); }
-
// Node versions
public static Node subject(Triple triple) {
return triple == null ? null : triple.getSubject();
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/GN.java b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/GN.java
index 6bb93ed..3b859de 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/GN.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/GN.java
@@ -24,6 +24,14 @@
import org.apache.jena.sparql.util.graph.GNode;
public class GN {
+
+ // Node filter tests.
+//public static boolean isURI(GNode n) { return n != null && isURI(n.getNode()); }
+//public static boolean isBlank(GNode n) { return n != null && isBlank(n.getNode()); }
+//public static boolean isLiteral(GNode n) { return n != null && isLiteral(n.getNode()); }
+//public static boolean isResource(GNode n) { return n != null && isURI(n.getNode())||isBlank(n.getNode()); }
+
+
public static GNode create(Graph graph, Node node) {
return new GNode(graph, node);
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java
index e10acb1..33264e7 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java
@@ -153,11 +153,11 @@
else
System.out.printf("Node=%s\n",displayStr(focusNode));
System.out.printf(" %s\n", msg);
-
+
Path path = null;
if ( pathNode != null )
path = ShaclPaths.parsePath(report.getModel().getGraph(), pathNode.asNode());
-
+
// Better (?) to build a report entry.
// ReportEntry e = ReportEntry.create()
// .focusNode(focusNode.asNode())
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ConstraintComponents.java b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ConstraintComponents.java
index ac1d07f..601186c 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ConstraintComponents.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ConstraintComponents.java
@@ -215,10 +215,19 @@
throw new ShaclParseException("SparqlConstraintComponent: Multiple SPARQL queries: "+displayStr(constraintComponentNode));
String prefixes = SparqlConstraints.prefixes(shapesGraph, valNode);
String queryString = firstNonNull(xSelect, xAsk).getLiteralLexicalForm().trim();
+ String message = asString(G.getZeroOrOneSP(shapesGraph, valNode, SHACL.message));
if ( ! prefixes.isEmpty() )
queryString = prefixes+"\n"+queryString;
boolean isSelect = (xSelect!=null);
- SparqlComponent cs = new SparqlComponent(constraintComponentNode, isSelect, queryString, params);
+ SparqlComponent cs = new SparqlComponent(constraintComponentNode, isSelect, queryString, params, message);
return cs;
}
+
+ private static String asString(Node x) {
+ if ( x == null )
+ return null;
+ if ( ! x.isLiteral() )
+ return null;
+ return x.getLiteralLexicalForm();
+ }
}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java
index 0b397bb..5a1929b 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java
@@ -76,6 +76,46 @@
// 4.8.2 sh:hasValue
// 4.8.3 sh:in
+ // The constraints that need just a single triple.
+ static Map<Node, ConstraintMaker> dispatch = new HashMap<>();
+ static {
+ dispatch.put( SHACL.class_, (g, s, p, o) -> new ClassConstraint(o) );
+ dispatch.put( SHACL.datatype, (g, s, p, o) -> new DatatypeConstraint(o) );
+ dispatch.put( SHACL.nodeKind, (g, s, p, o) -> new NodeKindConstraint(o) );
+ dispatch.put( SHACL.minCount, (g, s, p, o) -> new MinCount(intValue(o)) );
+ dispatch.put( SHACL.maxCount, (g, s, p, o) -> new MaxCount(intValue(o)) );
+
+ dispatch.put( SHACL.minInclusive, (g, s, p, o) -> new ValueMinInclusiveConstraint(o) );
+ dispatch.put( SHACL.minExclusive, (g, s, p, o) -> new ValueMinExclusiveConstraint(o) );
+ dispatch.put( SHACL.maxInclusive, (g, s, p, o) -> new ValueMaxInclusiveConstraint(o) );
+ dispatch.put( SHACL.maxExclusive, (g, s, p, o) -> new ValueMaxExclusiveConstraint(o) );
+
+ dispatch.put( SHACL.minLength, (g, s, p, o) -> new StrMinLengthConstraint(intValue(o)) );
+ dispatch.put( SHACL.maxLength, (g, s, p, o) -> new StrMaxLengthConstraint(intValue(o)) );
+ // in parseConstraint
+ //dispatch.put( SHACL.pattern, (g, p, o) -> notImplemented(p) );
+ dispatch.put( SHACL.languageIn, (g, s, p, o) -> new StrLanguageIn(listString(g, o)) );
+ dispatch.put( SHACL.uniqueLang, (g, s, p, o) -> new UniqueLangConstraint(booleanValueStrict(o)) );
+
+ dispatch.put( SHACL.hasValue, (g, s, p, o) -> new HasValueConstraint(o) );
+ dispatch.put( SHACL.in, (g, s, p, o) -> new InConstraint(list(g,o)) );
+ dispatch.put( SHACL.closed, (g, s, p, o) -> new ClosedConstraint(g,s,booleanValue(o)) );
+
+ dispatch.put( SHACL.equals, (g, s, p, o) -> new EqualsConstraint(o) );
+ dispatch.put( SHACL.disjoint, (g, s, p, o) -> new DisjointConstraint(o) );
+ dispatch.put( SHACL.lessThan, (g, s, p, o) -> new LessThanConstraint(o) );
+ dispatch.put( SHACL.lessThanOrEquals, (g, s, p, o) -> new LessThanOrEqualsConstraint(o) );
+
+ // Below
+ //dispatch.put( SHACL.not, (g, s, p, o) -> notImplemented(p) );
+ //dispatch.put( SHACL.and, (g, s, p, o) -> notImplemented(p) );
+ //dispatch.put( SHACL.or, (g, s, p, o) -> notImplemented(p) );
+ //dispatch.put( SHACL.xone, (g, s, p, o) -> notImplemented(p) );
+ //dispatch.put( SHACL.node, (g, s, p, o) -> notImplemented(p) );
+
+ dispatch.put(SHACL.sparql, (g, s, p, o) -> SparqlConstraints.parseSparqlConstraint(g, s, p, o) );
+ }
+
/**
* The constraints that just need an input node, and do not look in the data.
* For example, minCount is not here because needs all the instances to count them.
@@ -98,6 +138,12 @@
immediate.add(SHACL.pattern);
}
+ /**
+ * Entry point. Process all triples of a specific shape node (subject). Has
+ * access to map of parsed shapes so it can recursively call back into the shapes
+ * parser at when the constraint uses other shapes
+ * (sh:and/sh:or/sh:not/sh:xone.sh:node).
+ */
/*package*/ static List<Constraint> parseConstraints(Graph shapesGraph, Node shape, Map<Node, Shape> parsed) {
List<Constraint> constraints = new ArrayList<>();
Iterator<Triple> iter = G.find(shapesGraph, shape, null, null);
@@ -118,8 +164,13 @@
return constraints;
}
+ /**
+ * The translate of an RDF triple into a {@link Constraint}.
+ * Constraints require more that just the triple being inspected.
+ */
private static Constraint parseConstraint(Graph g, Node s, Node p, Node o, Map<Node, Shape> parsed) {
+ // Test for single triple constraints.
ConstraintMaker maker = dispatch.get(p);
if ( maker != null )
return maker.make(g, s, p, o);
@@ -154,6 +205,7 @@
return new ShNode(other);
}
+ // sh:pattern is influenced by an adjacent sh:flags.
if ( p.equals(SHACL.pattern) ) {
Node pat = o;
if ( ! Util.isSimpleString(pat) )
@@ -209,45 +261,6 @@
Constraint make(Graph g, Node s, Node p, Node o);
}
- static Map<Node, ConstraintMaker> dispatch = new HashMap<>();
- static {
- dispatch.put( SHACL.class_, (g, s, p, o) -> new ClassConstraint(o) );
- dispatch.put( SHACL.datatype, (g, s, p, o) -> new DatatypeConstraint(o) );
- dispatch.put( SHACL.nodeKind, (g, s, p, o) -> new NodeKindConstraint(o) );
- dispatch.put( SHACL.minCount, (g, s, p, o) -> new MinCount(intValue(o)) );
- dispatch.put( SHACL.maxCount, (g, s, p, o) -> new MaxCount(intValue(o)) );
-
- dispatch.put( SHACL.minInclusive, (g, s, p, o) -> new ValueMinInclusiveConstraint(o) );
- dispatch.put( SHACL.minExclusive, (g, s, p, o) -> new ValueMinExclusiveConstraint(o) );
- dispatch.put( SHACL.maxInclusive, (g, s, p, o) -> new ValueMaxInclusiveConstraint(o) );
- dispatch.put( SHACL.maxExclusive, (g, s, p, o) -> new ValueMaxExclusiveConstraint(o) );
-
- dispatch.put( SHACL.minLength, (g, s, p, o) -> new StrMinLengthConstraint(intValue(o)) );
- dispatch.put( SHACL.maxLength, (g, s, p, o) -> new StrMaxLengthConstraint(intValue(o)) );
- // in parseConstraint
- //dispatch.put( SHACL.pattern, (g, p, o) -> notImplemented(p) );
- dispatch.put( SHACL.languageIn, (g, s, p, o) -> new StrLanguageIn(listString(g, o)) );
- dispatch.put( SHACL.uniqueLang, (g, s, p, o) -> new UniqueLangConstraint(booleanValueStrict(o)) );
-
- dispatch.put( SHACL.hasValue, (g, s, p, o) -> new HasValueConstraint(o) );
- dispatch.put( SHACL.in, (g, s, p, o) -> new InConstraint(list(g,o)) );
- dispatch.put( SHACL.closed, (g, s, p, o) -> new ClosedConstraint(g,s,booleanValue(o)) );
-
- dispatch.put( SHACL.equals, (g, s, p, o) -> new EqualsConstraint(o) );
- dispatch.put( SHACL.disjoint, (g, s, p, o) -> new DisjointConstraint(o) );
- dispatch.put( SHACL.lessThan, (g, s, p, o) -> new LessThanConstraint(o) );
- dispatch.put( SHACL.lessThanOrEquals, (g, s, p, o) -> new LessThanOrEqualsConstraint(o) );
-
- // Below
- //dispatch.put( SHACL.not, (g, s, p, o) -> notImplemented(p) );
- //dispatch.put( SHACL.and, (g, s, p, o) -> notImplemented(p) );
- //dispatch.put( SHACL.or, (g, s, p, o) -> notImplemented(p) );
- //dispatch.put( SHACL.xone, (g, s, p, o) -> notImplemented(p) );
- //dispatch.put( SHACL.node, (g, s, p, o) -> notImplemented(p) );
-
- dispatch.put(SHACL.sparql, (g, s, p, o) -> SparqlConstraints.parseSparqlConstraint(g, s, p, o) );
- }
-
private static Constraint notImplemented(Node p) {
throw new NotImplemented(ShLib.displayStr(p));
}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Shape.java b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Shape.java
index 886557c..91a98fe 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Shape.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Shape.java
@@ -29,6 +29,7 @@
import org.apache.jena.shacl.engine.Target;
import org.apache.jena.shacl.engine.TargetOps;
import org.apache.jena.shacl.validation.Severity;
+import org.apache.jena.sparql.util.FmtUtils;
public abstract class Shape {
@@ -41,8 +42,8 @@
protected final List<Constraint> constraints;
protected final List<PropertyShape> propertyShapes;
- public Shape(Graph shapeGraph, Node shapeNode, boolean deactivated, Severity severity, List<Node> messages, Collection<Target> targets,
- List<Constraint> constraints, List<PropertyShape> propertyShapes) {
+ public Shape(Graph shapeGraph, Node shapeNode, boolean deactivated, Severity severity, List<Node> messages,
+ Collection<Target> targets, List<Constraint> constraints, List<PropertyShape> propertyShapes) {
super();
this.shapeGraph = shapeGraph;
this.shapeNode = shapeNode;
@@ -76,6 +77,10 @@
return targets;
}
+ public boolean hasTarget() {
+ return ! targets.isEmpty();
+ }
+
public List<Constraint> getConstraints() {
return constraints;
}
@@ -103,6 +108,8 @@
public void print(IndentedWriter out) {
printHeader(out);
+ out.print(" ");
+ out.print("node="+FmtUtils.stringForNode(shapeNode));
if ( deactivated() )
out.print(" deactivated");
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java
index 6c97866..10a0bfc 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java
@@ -58,18 +58,20 @@
private static final boolean DEBUG = false;
private static IndentedWriter OUT = IndentedWriter.stdout;
//private static Logger LOG = LoggerFactory.getLogger(ShapesParser.class);
-
+
/** Return a list of "top shapes", those with targets.
* The {@code shapesMap} is modified, adding in all shapes processed.
*/
public static Collection<Shape> parseShapes(Graph shapesGraph, Targets targets, Map<Node, Shape> shapesMap) {
Targets rootShapes = targets;
-
+
if ( DEBUG )
OUT.println("SparqlConstraintComponents");
ConstraintComponents sparqlConstraintComponents = ConstraintComponents.parseSparqlConstraintComponents(shapesGraph);
- List<Shape> acc = new ArrayList<>();
+ // LinkedHashMap - convenience, so shapes are kept in
+ // the order of discovery below.
+ Map<Node, Shape> acc = new LinkedHashMap<>();
if ( DEBUG )
OUT.println("sh:targetNodes");
@@ -112,42 +114,43 @@
}
});
}
-
+
// Syntax rules for well-formed shapes.
//https://www.w3.org/TR/shacl/#syntax-rules
- // Note - we only have the reachable shapes in "shapesMap".
-
- return acc ;
+ // Note - we only have the reachable shapes in "shapesMap".
+ return shapes(acc) ;
}
- /** Parse and add all the declared shapes into the map.
+ /** Parse and add all the declared shapes into the map.
* The {@code shapesMap} is modified, adding in all shapes processed.
*/
public static Collection<Shape> declaredShapes(Graph shapesGraph, Map<Node, Shape> shapesMap) {
// All declared shapes.
- List<Shape> acc = new ArrayList<>();
+ Map<Node, Shape> acc = new LinkedHashMap<>();
G.listAllNodesOfType(shapesGraph, SHACL.NodeShape).forEach(shapeNode->
parseRootShape(acc, shapesMap, shapesGraph, shapeNode));
G.listAllNodesOfType(shapesGraph, SHACL.PropertyShape).forEach(shapeNode->
parseRootShape(acc, shapesMap, shapesGraph, shapeNode));
- return acc;
+ return shapes(acc);
}
-
- private static void parseRootShape(List<Shape> acc, Map<Node, Shape> parsed, Graph shapesGraph, Node shNode) {
- if ( parsed.containsKey(shNode) )
- return ;
+
+ private static Collection<Shape> shapes(Map<Node, Shape> acc) {
+ // The list will be in insertion order.
+ return new ArrayList<>(acc.values());
+ }
+
+ private static void parseRootShape(Map<Node, Shape> acc, Map<Node, Shape> parsed, Graph shapesGraph, Node shNode) {
+ if ( acc.containsKey(shNode) )
+ // Already processed as root shape.
+ return;
if ( DEBUG )
OUT.incIndent();
Shape shape = parseShapeStep(parsed, shapesGraph, shNode);
- acc.add(shape);
+ acc.put(shNode, shape);
if ( DEBUG )
OUT.decIndent();
}
-// public static Shape parseShape(Graph shapesGraph, Map<Node, Shape> parsed, Node shNode) {
-// return parseShape(parsed, shapesGraph, shNode);
-// }
-
/** Parse a specific shape from the Shapes graph */
public static Shape parseShape(Graph shapesGraph, Node shNode) {
// Avoid recursion.
@@ -222,9 +225,9 @@
OUT.printf("Node shape %s\n", displayStr(shapeNode));
return new NodeShape(shapesGraph, shapeNode, isDeactivated, severity, messages, targets, constraints, propertyShapes);
}
-
+
// -- Property shape.
-
+
if ( DEBUG )
OUT.incIndent();
Node pathNode = getOneSP(shapesGraph, shapeNode, SHACL.path);
@@ -280,21 +283,20 @@
for ( Triple t : propertyTriples) {
// Must be a property shape.
Node propertyShape = object(t);
-
+
long x = countSP(shapesGraph, propertyShape, SHACL.path);
- if ( x == 0 )
- throw new ShaclParseException("No sh:path on a property shape: "+displayStr(shapeNode));
+ if ( x == 0 ) {
+ // Is it a typo? -> Can we find it as a subject?
+ boolean existsAsSubject = G.contains(shapesGraph, propertyShape, null,null);
+ if ( ! existsAsSubject )
+ throw new ShaclParseException("Missing property shape: node="+displayStr(shapeNode)+" sh:property "+displayStr(propertyShape));
+ else
+ throw new ShaclParseException("No sh:path on a property shape: node="+displayStr(shapeNode)+" sh:property "+displayStr(propertyShape));
+ }
if ( x > 1 ) {
List<Node> paths = listSP(shapesGraph, propertyShape, SHACL.path);
- throw new ShaclParseException("Muiltiple sh:path on a property shape: "+displayStr(shapeNode)+ " : "+paths);
+ throw new ShaclParseException("Muiltiple sh:path on a property shape: "+displayStr(shapeNode)+" sh:property"+displayStr(propertyShape)+ " : "+paths);
}
-// if ( DEBUG ) {
-// Node pathNode = G.getSP(shapesGraph, propertyShape, SHACL.path);
-// if ( pathNode != null ) {
-// Path path = parsePath(shapesGraph, pathNode);
-// OUT.printf("Found property shape: path = %s\n", pathToString(shapesGraph, path));
-// }
-// }
PropertyShape ps = (PropertyShape)parseShapeStep(parsed, shapesGraph, propertyShape);
propertyShapes.add(ps);
}
@@ -313,9 +315,9 @@
accTarget(x, shapesGraph, shape, TargetType.targetClass);
accTarget(x, shapesGraph, shape, TargetType.targetObjectsOf);
accTarget(x, shapesGraph, shape, TargetType.targetSubjectsOf);
-
+
// TargetType.implicitClass : some overlap with TargetOps.implicitClassTargets
- // Explicitly sh:NodeShape or sh:PropertyShape and also subClassof* rdfs:Class.
+ // Explicitly sh:NodeShape or sh:PropertyShape and also subClassof* rdfs:Class.
if ( isShapeType(shapesGraph, shape) && isOfType(shapesGraph, shape, rdfsClass) )
x.add(Target.create(TargetType.implicitClass, shape));
return x;
@@ -324,7 +326,7 @@
private static boolean isShapeType(Graph shapesGraph, Node shape) {
return hasType(shapesGraph, shape, SHACL.NodeShape) || hasType(shapesGraph, shape, SHACL.PropertyShape);
}
-
+
private static Severity severity(Graph shapesGraph, Node shNode) {
Node sev = G.getSP(shapesGraph, shNode, SHACL.severity);
if ( sev == null )
@@ -339,5 +341,4 @@
.forEachRemaining(target->acc.add(target));
} finally { iter.close(); }
}
-
}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationProc.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationProc.java
index 7c862fe..1082cdd 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationProc.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationProc.java
@@ -113,6 +113,11 @@
return vCxt.generateReport();
}
+ public static ValidationReport simpleValidation(Graph shapesGraph, Graph dataGraph, Node node, boolean verbose) {
+ Shapes shapes = Shapes.parse(shapesGraph);
+ return simpleValidationNode(shapes, dataGraph, node, verbose);
+ }
+
public static void simpleValidation(ValidationContext vCxt, Graph data, Shape shape) {
simpleValidationInternal(vCxt, data, null, shape);
}
@@ -125,7 +130,6 @@
ValidationContext vCxt = new ValidationContext(shapes, data);
vCxt.setVerbose(verbose);
return simpleValidationNode(vCxt, shapes, node, data);
- //} catch (ShaclParseException ex) {
} finally { out.setAbsoluteIndent(x); }
}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/testing/CmdTest.java b/jena-shacl/src/test/java/org/apache/jena/shacl/testing/CmdTest.java
deleted file mode 100644
index 3ca6574..0000000
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/testing/CmdTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.shacl.testing;
-
-public class CmdTest {
- public static void main(String...a) {
- for ( String fn : a )
- RunManifest.runTest(fn);
- }
-}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/testing/RunManifest.java b/jena-shacl/src/test/java/org/apache/jena/shacl/testing/RunManifest.java
index 71315fe..5d06b58 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/testing/RunManifest.java
+++ b/jena-shacl/src/test/java/org/apache/jena/shacl/testing/RunManifest.java
@@ -33,7 +33,7 @@
}
public static void runTest(String manifest, boolean verbose) {
- if ( true ) {
+ if ( verbose ) {
try {
String fn = manifest;
if ( manifest.startsWith("file://" ) )
diff --git a/jena-shacl/src/test/resources/local/additional/target-target-1.ttl b/jena-shacl/src/test/resources/local/additional/target-target-1.ttl
new file mode 100644
index 0000000..8886660
--- /dev/null
+++ b/jena-shacl/src/test/resources/local/additional/target-target-1.ttl
@@ -0,0 +1,62 @@
+# Test iof nested targets.
+
+PREFIX ex: <http://example/>
+
+PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+
+PREFIX sh: <http://www.w3.org/ns/shacl#>
+PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
+
+PREFIX sht: <http://www.w3.org/ns/shacl-test#>
+PREFIX mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#>
+
+ex:A rdf:type ex:MyClass .
+
+ex:Shape1
+ sh:targetClass ex:MyClass ;
+ sh:node ex:Shape2 ;
+ .
+
+ex:Shape2
+ sh:targetClass ex:MyClass ;
+ sh:node ex:Shape3 ;
+ .
+
+ex:Shape3
+ sh:property [
+ sh:path ex:p ;
+ sh:minCount 1 ;
+ ] ;
+ .
+
+<>
+ rdf:type mf:Manifest ;
+ mf:entries ( <target-class-subclass-1> )
+ .
+
+<target-class-subclass-1>
+ rdf:type sht:Validate ;
+ rdfs:label "Target links to target, which links to constraint : run twice." ;
+ mf:action [
+ sht:dataGraph <> ;
+ sht:shapesGraph <> ;
+ ] ;
+ mf:result [
+ rdf:type sh:ValidationReport ;
+ sh:conforms false ;
+ sh:result [ a sh:ValidationResult ;
+ sh:focusNode ex:A ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraintComponent sh:NodeConstraintComponent ;
+ sh:sourceShape ex:Shape2 ;
+ sh:value ex:A
+ ] ;
+ sh:result [ a sh:ValidationResult ;
+ sh:focusNode ex:A ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraintComponent sh:NodeConstraintComponent ;
+ sh:sourceShape ex:Shape1 ;
+ sh:value ex:A
+ ]
+] .
\ No newline at end of file
diff --git a/jena-shacl/src/test/resources/local/manifest.ttl b/jena-shacl/src/test/resources/local/manifest.ttl
index 45479b7..1b04264 100644
--- a/jena-shacl/src/test/resources/local/manifest.ttl
+++ b/jena-shacl/src/test/resources/local/manifest.ttl
@@ -12,4 +12,5 @@
mf:include <additional/lang-simple-1.ttl> ;
mf:include <additional/implicit-subclass-1.ttl> ;
mf:include <additional/target-class-subclass-1.ttl> ;
+ mf:include <additional/target-target-1.ttl> ;
.