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> ;
 .