You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2022/07/11 09:55:37 UTC

[jena] branch main updated: Add SHACL ValidationListener

This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new b01c375521 Add SHACL ValidationListener
     new 47929688b4 Merge pull request #1256 from fkleedorfer/JENA-2320
b01c375521 is described below

commit b01c3755219600c43e76cc351f29f13124f1c376
Author: Florian Kleedorfer <fl...@austria.fm>
AuthorDate: Mon Apr 11 08:56:59 2022 +0200

    Add SHACL ValidationListener
    
    * Modifies ValidationContext so it can be given a ValidationListener upon creation. No listener
    * SHACL Validation algorithm calls the ValidationListener at many points during the validation
    * The class hierarchy under ValidationEvent represents the different possible cases and can be used in a listener implementation to filter events
    * Tried to minimize performance impact by not generating any events when no listener is present, and by generating copies of collections in events only when they are actually used by clients.
    * Adds a test class that check events emitted during SHACL validation
    * Adds a ValidationListener implementation that can be configured with event handlers to react to the events of interest.
    * Adds the validation package to generated javadoc
---
 jena-shacl/pom.xml                                 |   2 +-
 .../jena/shacl/engine/ValidationContext.java       |  32 +-
 .../shacl/engine/constraint/ClosedConstraint.java  |   8 +
 .../engine/constraint/ConstraintDataTerm.java      |  14 +-
 .../shacl/engine/constraint/ConstraintEntity.java  |   6 +-
 .../jena/shacl/engine/constraint/ConstraintOp.java |  13 +-
 .../engine/constraint/ConstraintPairwise.java      |  34 ++
 .../shacl/engine/constraint/ConstraintTerm.java    |  12 +-
 .../engine/constraint/DisjointConstraint.java      |  22 +
 .../shacl/engine/constraint/EqualsConstraint.java  |  24 +-
 .../engine/constraint/HasValueConstraint.java      |   6 +
 .../engine/constraint/LessThanConstraint.java      |   6 +
 .../constraint/LessThanOrEqualsConstraint.java     |  11 +
 .../engine/constraint/QualifiedValueShape.java     |  64 ++-
 .../shacl/engine/constraint/SparqlValidation.java  |  21 +-
 .../engine/constraint/UniqueLangConstraint.java    |   9 +
 .../validation/HandlerBasedValidationListener.java | 233 +++++++++++
 .../org/apache/jena/shacl/validation/VLib.java     |  14 +-
 .../jena/shacl/validation/ValidationListener.java} |  26 +-
 .../jena/shacl/validation/ValidationProc.java      |  42 +-
 .../event/AbstractConstraintEvaluationEvent.java   |  60 +++
 .../AbstractConstraintEvaluationForPathEvent.java  |  62 +++
 ...stractConstraintEvaluationOnPathNodesEvent.java |  64 +++
 ...tConstraintEvaluationOnSinglePathNodeEvent.java |  62 +++
 .../event/AbstractFocusNodeValidationEvent.java    |  62 +++
 .../event/AbstractShapeValidationEvent.java        |  60 +++
 .../event/AbstractTargetShapesValidationEvent.java |  58 +++
 .../validation/event/AbstractValidationEvent.java  |  52 +++
 .../shacl/validation/event/CompareNodesEvent.java} |  27 +-
 .../event/ConstraintEvaluatedEvent.java}           |  28 +-
 .../event/ConstraintEvaluatedOnFocusNodeEvent.java |  76 ++++
 ...tEvaluatedOnFocusNodeWithCompareNodesEvent.java |  72 ++++
 ...uatedOnFocusNodeWithSingleCompareNodeEvent.java |  73 ++++
 .../event/ConstraintEvaluatedOnPathNodesEvent.java |  75 ++++
 ...tEvaluatedOnPathNodesWithCompareNodesEvent.java |  76 ++++
 .../ConstraintEvaluatedOnSinglePathNodeEvent.java  |  75 ++++
 ...uatedOnSinglePathNodeWithCompareNodesEvent.java |  79 ++++
 ...OnSinglePathNodeWithSingleCompareNodeEvent.java |  76 ++++
 .../event/ConstraintEvaluationEvent.java}          |  23 +-
 ...straintEvaluationForNodeShapeFinishedEvent.java |  43 ++
 ...nstraintEvaluationForNodeShapeStartedEvent.java |  43 ++
 .../event/ConstraintEvaluationForPathEvent.java}   |  23 +-
 ...ntEvaluationForPropertyShapeFinishedEvent.java} |  42 +-
 ...intEvaluationForPropertyShapeStartedEvent.java} |  42 +-
 .../ConstraintEvaluationOnPathNodesEvent.java}     |  23 +-
 ...ConstraintEvaluationOnSinglePathNodeEvent.java} |  23 +-
 .../shacl/validation/event/EventPredicates.java    | 105 +++++
 .../jena/shacl/validation/event/EventUtil.java     |  49 +++
 .../event/FocusNodeValidationEvent.java}           |  26 +-
 .../event/FocusNodeValidationFinishedEvent.java}   |  36 +-
 .../event/FocusNodeValidationStartedEvent.java}    |  36 +-
 .../event/FocusNodesDeterminedEvent.java           |  66 +++
 .../event/ImmutableLazyCollectionCopy.java}        |  39 +-
 .../validation/event/ImmutableLazySetCopy.java}    |  36 +-
 .../validation/event/ShapeValidationEvent.java}    |  23 +-
 .../event/ShapeValidationFinishedEvent.java}       |  33 +-
 .../event/ShapeValidationStartedEvent.java}        |  33 +-
 .../validation/event/SingleCompareNodeEvent.java}  |  23 +-
 .../event/TargetShapeValidationEvent.java}         |  23 +-
 .../TargetShapesValidationFinishedEvent.java}      |  36 +-
 .../event/TargetShapesValidationStartedEvent.java} |  36 +-
 .../shacl/validation/event/ValidationEvent.java}   |  26 +-
 .../event/ValidationLifecycleEvent.java}           |  26 +-
 .../ValueNodesDeterminedForPropertyShapeEvent.java |  79 ++++
 .../test/java/org/apache/jena/shacl/TC_SHACL.java  |   2 +
 .../jena/shacl/tests/ValidationListenerTests.java  | 462 +++++++++++++++++++++
 66 files changed, 2741 insertions(+), 452 deletions(-)

diff --git a/jena-shacl/pom.xml b/jena-shacl/pom.xml
index 1de4502750..507bb7563e 100644
--- a/jena-shacl/pom.xml
+++ b/jena-shacl/pom.xml
@@ -133,7 +133,7 @@
           <encoding>UTF-8</encoding>
           <windowtitle>Apache Jena TDB</windowtitle>
           <doctitle>Apache Jena - SHACL ${project.version}</doctitle>
-          <excludePackageNames>org.apache.jena.shacl.*</excludePackageNames>
+          <excludePackageNames>*.compact:*.compact.*:*.engine:*.engine.*:*.lib:*.parser:*.sys:*.vocabulary</excludePackageNames>
           <bottom>Licenced under the Apache License, Version 2.0</bottom>
         </configuration>
       </plugin>
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/ValidationContext.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/ValidationContext.java
index 7dbbc538fc..c9ffa4f2d5 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/ValidationContext.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/ValidationContext.java
@@ -27,8 +27,12 @@ import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.sys.ShaclSystem;
 import org.apache.jena.shacl.validation.ReportItem;
+import org.apache.jena.shacl.validation.ValidationListener;
+import org.apache.jena.shacl.validation.event.ValidationEvent;
 import org.apache.jena.sparql.path.Path;
 
+import java.util.function.Supplier;
+
 public class ValidationContext {
 
     public static boolean VERBOSE = false;
@@ -39,15 +43,24 @@ public class ValidationContext {
     private final Shapes shapes;
     private final Graph dataGraph;
     private boolean strict = false;
+    private final ValidationListener validationListener;
 
     private final ErrorHandler errorHandler;
 
     public static ValidationContext create(Shapes shapes, Graph data) {
-        return create(shapes, data, ShaclSystem.systemShaclErrorHandler);
+        return create(shapes, data, ShaclSystem.systemShaclErrorHandler, null);
     }
-    
-    public static ValidationContext create(Shapes shapes, Graph data, ErrorHandler errorHandler) {
-        ValidationContext vCxt = new ValidationContext(shapes, data, errorHandler);
+
+    public static ValidationContext create(Shapes shapes, Graph data, ValidationListener validationListener) {
+        return create(shapes, data, null, validationListener);
+    }
+
+    public static ValidationContext create(Shapes shapes, Graph data, ErrorHandler errorHandler){
+        return create(shapes, data, errorHandler, null);
+    }
+
+    public static ValidationContext create(Shapes shapes, Graph data, ErrorHandler errorHandler, ValidationListener validationListener) {
+        ValidationContext vCxt = new ValidationContext(shapes, data, errorHandler, validationListener);
         vCxt.setVerbose(VERBOSE);
         return vCxt;
     }
@@ -62,14 +75,16 @@ public class ValidationContext {
         this.verbose = vCxt.verbose;
         this.strict = vCxt.strict;
         this.errorHandler = vCxt.errorHandler;
+        this.validationListener = vCxt.validationListener;
     }
 
-    private ValidationContext(Shapes shapes, Graph data, ErrorHandler errorHandler) {
+    private ValidationContext(Shapes shapes, Graph data, ErrorHandler errorHandler, ValidationListener validationListener) {
         this.shapes = shapes;
         this.dataGraph = data;
         if ( errorHandler == null )
             errorHandler = ShaclSystem.systemShaclErrorHandler;
         this.errorHandler = errorHandler;
+        this.validationListener = validationListener;
         validationReportBuilder.addPrefixes(data.getPrefixMapping());
         validationReportBuilder.addPrefixes(shapes.getGraph().getPrefixMapping());
     }
@@ -124,4 +139,11 @@ public class ValidationContext {
         return errorHandler;
     }
 
+    public void notifyValidationListener(Supplier<ValidationEvent> eventSupplier){
+        if (validationListener != null){
+            ValidationEvent event = eventSupplier.get();
+            validationListener.onValidationEvent(event);
+        }
+    }
+
 }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ClosedConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ClosedConstraint.java
index d3522b9450..dc3cc24652 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ClosedConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ClosedConstraint.java
@@ -35,6 +35,8 @@ import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.ShaclParseException;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnFocusNodeEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.path.Path;
 import org.apache.jena.sparql.path.PathFactory;
@@ -108,15 +110,21 @@ public class ClosedConstraint implements Constraint {
         // is a bnode (usually).
 
         Set<Node> actual = properties(data,  focusNode);
+        boolean passed = true;
         for ( Node p : actual ) {
             if ( ! expected.contains(p) && ! ignoredProperties.contains(p) ) {
                 Path path = PathFactory.pathLink(p);
+                passed = false;
                 G.listSP(data, focusNode, p).forEach(o-> {
                     String msg = toString()+" Property = "+displayStr(p)+" : Object = "+displayStr(o);
+                    vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, this, path, o,false));
                     vCxt.reportEntry(msg, shape, focusNode, path, o, this);
                 });
             }
         }
+        if (passed) {
+            vCxt.notifyValidationListener(() ->  new ConstraintEvaluatedOnFocusNodeEvent(vCxt, shape, focusNode, this, true));
+        }
     }
 
     private Set<Node> properties(Graph data, Node focusNode) {
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintDataTerm.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintDataTerm.java
index ec304704ab..7a9ff0e1ba 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintDataTerm.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintDataTerm.java
@@ -18,16 +18,18 @@
 
 package org.apache.jena.shacl.engine.constraint;
 
-import java.util.Set;
-
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.validation.ReportItem;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnFocusNodeEvent;
 import org.apache.jena.sparql.path.Path;
 
+import java.util.Set;
+
 /**
  * A restriction on an RDF term which needs access to the data to check such as
  * {@link ClassConstraint}.
@@ -53,7 +55,13 @@ public abstract class ConstraintDataTerm implements Constraint {
 
     private void applyConstraintDataTerm(ValidationContext vCxt, Shape shape, Graph data, Node focusNode, Path path, Node term) {
         ReportItem item = validate(vCxt, data, term);
-        if ( item == null )
+        boolean passed = item == null;
+        if (path == null) {
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnFocusNodeEvent(vCxt, shape, focusNode, this, passed));
+        } else {
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, this, path, term, passed));
+        }
+        if ( passed )
             return;
         vCxt.reportEntry(item.getMessage(), shape, focusNode, path, item.getValue(), this);
     }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintEntity.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintEntity.java
index 4551e9b004..eee0a62076 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintEntity.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintEntity.java
@@ -26,6 +26,7 @@ import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.validation.ReportItem;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnPathNodesEvent;
 import org.apache.jena.sparql.path.Path;
 
 /** A Constraint that handles an RDF "entity" (e.g. triples with the same subject)
@@ -46,7 +47,10 @@ public abstract class ConstraintEntity implements Constraint {
     final
     public void validatePropertyShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode, Path path, Set<Node> pathNodes) {
         ReportItem item = validate(vCxt, pathNodes);
-        if ( item == null)
+        boolean passed = item == null;
+        vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnPathNodesEvent(vCxt, shape, focusNode, this, path, pathNodes,
+                        passed));
+        if ( passed )
             return;
         vCxt.reportEntry(item.getMessage(), shape, focusNode, path, item.getValue(), this);
     }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
index e8715a32c2..2edeeda92a 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
@@ -26,6 +26,8 @@ import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.validation.ReportItem;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnFocusNodeEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
 import org.apache.jena.sparql.path.Path;
 
 /** A constraint that combines other constraints */
@@ -35,8 +37,11 @@ public abstract class ConstraintOp implements Constraint {
     final
     public void validateNodeShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
         ReportItem item = validate(vCxt, data, focusNode);
-        if ( item != null )
+        boolean passed = item == null;
+        if ( ! passed ) {
             vCxt.reportEntry(item, shape, focusNode, null, this);
+        }
+        vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnFocusNodeEvent(vCxt, shape, focusNode, this, passed));
     }
 
     @Override
@@ -44,8 +49,12 @@ public abstract class ConstraintOp implements Constraint {
     public void validatePropertyShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode, Path path, Set<Node> pathNodes) {
         pathNodes.forEach(n-> {
             ReportItem item = validate(vCxt, data, n);
-            if ( item != null )
+            boolean passed = item == null;
+            if ( ! passed ) {
                 vCxt.reportEntry(item, shape, focusNode, path, this);
+            }
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, this, path, n,
+                            passed));
         });
     }
 
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintPairwise.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintPairwise.java
index 85268a8f9c..21c44d1a92 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintPairwise.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintPairwise.java
@@ -28,6 +28,7 @@ import org.apache.jena.riot.other.G;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.*;
 import org.apache.jena.sparql.expr.ExprNotComparableException;
 import org.apache.jena.sparql.expr.NodeValue;
 import org.apache.jena.sparql.path.Path;
@@ -69,6 +70,39 @@ public abstract class ConstraintPairwise implements Constraint {
         validate(vCxt, shape, focusNode, path, pathNodes, compareNodes);
     }
 
+    protected AbstractConstraintEvaluationEvent makeEventSingleCompareNode(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Path path, Node valueNode, Node compareNode, boolean isValid) {
+        if (valueNode.equals(focusNode)) {
+            return new ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent(vCxt, shape, focusNode, this,
+                            compareNode, isValid);
+        }
+        return new ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent(vCxt, shape,
+                        focusNode, this, path, valueNode, compareNode,
+                        isValid);
+    }
+
+    protected AbstractConstraintEvaluationEvent makeEvent(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Path path, Set<Node> pathNodes, Set<Node> compareNodes, boolean isValid) {
+        if (pathNodes.size() == 1 && pathNodes.contains(focusNode)) {
+            return new ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent(vCxt, shape, focusNode, this,
+                            compareNodes, isValid);
+        }
+        return new ConstraintEvaluatedOnPathNodesWithCompareNodesEvent(vCxt, shape,
+                        focusNode, this, path, pathNodes, compareNodes,
+                        isValid);
+    }
+
+    protected AbstractConstraintEvaluationEvent makeEventSinglePathNode(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Path path, Node pathNode, Set<Node> compareNodes, boolean isValid) {
+        if (pathNode.equals(focusNode)) {
+            return new ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent(vCxt, shape, focusNode, this,
+                            compareNodes, isValid);
+        }
+        return new ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent(vCxt, shape,
+                        focusNode, this, path, pathNode, compareNodes,
+                        isValid);
+    }
+
     public abstract void validate(ValidationContext vCxt, Shape shape, Node focusNode, Path path,
                                   Set<Node> pathNodes, Set<Node> compareNodes);
 
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintTerm.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintTerm.java
index e02da871c0..ec12616eab 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintTerm.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintTerm.java
@@ -26,6 +26,8 @@ import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.validation.ReportItem;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnFocusNodeEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
 import org.apache.jena.sparql.path.Path;
 
 /* Constraint that does not need access to the data other than the nodes supplied. e.g. sh:datatype. */
@@ -47,8 +49,16 @@ public abstract class ConstraintTerm implements Constraint {
 
     private void applyConstraintTerm(ValidationContext vCxt, Shape shape, Node focusNode, Path path, Node term) {
         ReportItem item = validate(vCxt, term);
-        if ( item == null )
+        boolean passed = item == null;
+        if (path == null) {
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnFocusNodeEvent(vCxt, shape, focusNode, this, passed));
+        } else {
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, this, path,
+                                            term, passed));
+        }
+        if ( passed ) {
             return;
+        }
         vCxt.reportEntry(item, shape, focusNode, path, this);
     }
 
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/DisjointConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/DisjointConstraint.java
index 121f65152b..c51ae63f51 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/DisjointConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/DisjointConstraint.java
@@ -30,9 +30,17 @@ import org.apache.jena.riot.out.NodeFormatter;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnPathNodesWithCompareNodesEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.path.Path;
 
+import java.util.Objects;
+import java.util.Set;
+
+import static org.apache.jena.shacl.compact.writer.CompactOut.compactArrayNodes;
+import static org.apache.jena.shacl.lib.ShLib.displayStr;
+
 /** sh:disjoint */
 public class DisjointConstraint extends ConstraintPairwise {
 
@@ -43,11 +51,25 @@ public class DisjointConstraint extends ConstraintPairwise {
     @Override
     public void validate(ValidationContext vCxt, Shape shape, Node focusNode, Path path,
                          Set<Node> pathNodes, Set<Node> compareNodes) {
+        boolean allPassed = true;
         for ( Node vn : pathNodes ) {
+            boolean passed = true;
             if ( compareNodes.contains(vn) ) {
                 String msg = toString()+": not disjoint: "+displayStr(vn)+" is in "+compareNodes;
+                passed = false;
+                allPassed = false;
                 vCxt.reportEntry(msg, shape, focusNode, path, vn, this);
             }
+            if (!passed) {
+                vCxt.notifyValidationListener(() -> makeEventSinglePathNode(
+                                                vCxt, shape, focusNode, path, vn,
+                                                compareNodes, false));
+            }
+        }
+        if (allPassed){
+            vCxt.notifyValidationListener(() -> makeEvent(
+                                            vCxt, shape, focusNode, path, pathNodes,
+                                            compareNodes, true));
         }
     }
 
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/EqualsConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/EqualsConstraint.java
index 42f170a183..c58e7230cd 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/EqualsConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/EqualsConstraint.java
@@ -18,21 +18,22 @@
 
 package org.apache.jena.shacl.engine.constraint;
 
-import static org.apache.jena.shacl.compact.writer.CompactOut.compact;
-import static org.apache.jena.shacl.lib.ShLib.displayStr;
-
-import java.util.Objects;
-import java.util.Set;
-
 import org.apache.jena.atlas.io.IndentedWriter;
 import org.apache.jena.graph.Node;
 import org.apache.jena.riot.out.NodeFormatter;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.path.Path;
 
+import java.util.Objects;
+import java.util.Set;
+
+import static org.apache.jena.shacl.compact.writer.CompactOut.compact;
+import static org.apache.jena.shacl.lib.ShLib.displayStr;
+
 /** sh:equals */
 public class EqualsConstraint extends ConstraintPairwise {
 
@@ -44,16 +45,27 @@ public class EqualsConstraint extends ConstraintPairwise {
     public void validate(ValidationContext vCxt, Shape shape, Node focusNode, Path path,
                          Set<Node> pathNodes, Set<Node> compareNodes) {
         for ( Node vn : pathNodes ) {
+            boolean passed = true;
             if ( ! compareNodes.contains(vn) ) {
                 String msg = toString()+": not equal: value node "+displayStr(vn)+" is not in "+compareNodes;
+                passed = false;
                 vCxt.reportEntry(msg, shape, focusNode, path, vn, this);
             }
+            final boolean finalPassed = passed;
+            vCxt.notifyValidationListener(() ->  makeEventSinglePathNode(vCxt, shape,  focusNode, path,
+                                vn, compareNodes, finalPassed));
+
         }
         for ( Node v : compareNodes ) {
+            boolean passed = true;
             if ( ! pathNodes.contains(v) ) {
                 String msg = toString()+": not equal: value "+displayStr(v)+" is not in "+pathNodes;
+                passed = false;
                 vCxt.reportEntry(msg, shape, focusNode, path, v, this);
             }
+            final boolean finalPassed = passed;
+            vCxt.notifyValidationListener(() -> makeEventSinglePathNode(vCxt, shape, focusNode, path,
+                            v, pathNodes, finalPassed));
         }
     }
 
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/HasValueConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/HasValueConstraint.java
index 9a97c76684..a43852779b 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/HasValueConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/HasValueConstraint.java
@@ -32,6 +32,7 @@ import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.validation.ReportItem;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnFocusNodeEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 
 /** sh:hasValue */
@@ -56,10 +57,15 @@ public class HasValueConstraint extends ConstraintEntity {
     // NodeShape usage.
     @Override
     public void validateNodeShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
+        boolean passed = true;
         if ( ! focusNode.equals(value) ) {
+            passed = false;
             String errMsg = toString()+" : No value "+displayStr(value);
             vCxt.reportEntry(errMsg, shape, focusNode, null, null, this);
         }
+        final boolean finalPassed = passed;
+        vCxt.notifyValidationListener(() -> 
+                        new ConstraintEvaluatedOnFocusNodeEvent(vCxt, shape, focusNode,  this,   finalPassed));
     }
 
     // PropertyShape usage.
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanConstraint.java
index e5a9ac83d0..7f7240c63a 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanConstraint.java
@@ -30,6 +30,7 @@ import org.apache.jena.riot.out.NodeFormatter;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.expr.Expr;
 import org.apache.jena.sparql.path.Path;
@@ -47,10 +48,15 @@ public class LessThanConstraint extends ConstraintPairwise {
         for ( Node vn : pathNodes ) {
             for ( Node v : compareNodes ) {
                 int r = super.compare(vn, v) ;
+                boolean passed = true;
                 if ( r != Expr.CMP_LESS ) {
+                    passed = false;
                     String msg = toString()+": value node "+displayStr(vn)+" is not less than "+displayStr(v);
                     vCxt.reportEntry(msg, shape, focusNode, path, vn, this);
                 }
+                final boolean finalPassed = passed;
+                vCxt.notifyValidationListener(() -> makeEventSingleCompareNode(vCxt, shape, focusNode,
+                                                path, vn, v, finalPassed));
             }
         }
     }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanOrEqualsConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanOrEqualsConstraint.java
index ce4f906e8a..785cdd90cf 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanOrEqualsConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/LessThanOrEqualsConstraint.java
@@ -30,6 +30,10 @@ import org.apache.jena.riot.out.NodeFormatter;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.AbstractConstraintEvaluationEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnPathNodesWithCompareNodesEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.expr.Expr;
 import org.apache.jena.sparql.path.Path;
@@ -47,10 +51,17 @@ public class LessThanOrEqualsConstraint extends ConstraintPairwise {
         for ( Node vn : pathNodes ) {
             for ( Node v : compareNodes ) {
                 int r = super.compare(vn, v) ;
+                boolean passed = true;
                 if ( r != Expr.CMP_LESS && r != Expr.CMP_EQUAL ) {
+                    passed = false;
                     String msg = toString()+": value node "+displayStr(vn)+" is not less than or equal to "+displayStr(v);
                     vCxt.reportEntry(msg, shape, focusNode, path, vn, this);
                 }
+                final boolean finalPassed = passed;
+                    vCxt.notifyValidationListener(() ->
+                                    makeEventSingleCompareNode(vCxt, shape,
+                                                    focusNode, path, vn, v,
+                                                    finalPassed));
             }
         }
     }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/QualifiedValueShape.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/QualifiedValueShape.java
index bc654092fb..f625d0b8bc 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/QualifiedValueShape.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/QualifiedValueShape.java
@@ -34,6 +34,7 @@ import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.validation.ValidationProc;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnPathNodesWithCompareNodesEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.path.Path;
 
@@ -122,17 +123,34 @@ public class QualifiedValueShape implements Constraint {
             if ( b )
                 x++;
         }
-
+        boolean passed = true;
         if ( qMin >= 0 && qMin > x ) {
+            passed = false;
             String msg = toString()+": Min = "+qMin+" but got "+x+" validations";
             vCxt.reportEntry(msg, shape, focusNode, path, null,
                 new ReportConstraint(SHACL.QualifiedMinCountConstraintComponent));
         }
+        final int finalX = x;
+    boolean finalPassed = passed;
+        if (qMin > 0) {
+            vCxt.notifyValidationListener(() -> new QualifiedMinCountConstraintEvaluatedEvent(vCxt, shape,
+                                            focusNode, this, path, valueNodes, valueNodes2, qMin, finalX,
+                                            finalPassed));
+        }
+        passed = true;
         if ( qMax >= 0 && qMax < x ) {
+            passed = false;
             String msg = toString()+": Max = "+qMax+" but got "+x+" validations";
             vCxt.reportEntry(msg, shape, focusNode, path, null,
                 new ReportConstraint(SHACL.QualifiedMaxCountConstraintComponent));
         }
+        if (qMax > 0) {
+            boolean finalPassed2 = passed;
+            vCxt.notifyValidationListener(() -> new QualifiedMaxCountConstraintEvaluatedEvent(vCxt, shape,
+                                            focusNode, this, path, valueNodes, valueNodes2, qMax, finalX,
+                                            finalPassed2));
+        }
+        
     }
 
     private boolean conformsSiblings(ValidationContext vCxt, Node v, Collection<Node> sibs) {
@@ -213,4 +231,48 @@ public class QualifiedValueShape implements Constraint {
             (qMax<0) ? "_" : Integer.toString(qMax),
             qDisjoint);
     }
+    
+    public static class QualifiedMinCountConstraintEvaluatedEvent extends
+                    ConstraintEvaluatedOnPathNodesWithCompareNodesEvent {
+        final int minCount;
+        final int actualCount;
+
+        public QualifiedMinCountConstraintEvaluatedEvent(ValidationContext vCxt, Shape shape,
+                        Node focusNode, Constraint constraint, Path path, Set<Node> valueNodes,
+                        Set<Node> compareNodes, int minCount, int actualCount, boolean valid) {
+            super(vCxt, shape, focusNode, constraint, path, valueNodes, compareNodes, valid);
+            this.minCount = minCount;
+            this.actualCount = actualCount;
+        }
+
+        public int getMinCount() {
+            return minCount;
+        }
+
+        public int getActualCount() {
+            return actualCount;
+        }
+    }
+
+    public static class QualifiedMaxCountConstraintEvaluatedEvent extends
+                    ConstraintEvaluatedOnPathNodesWithCompareNodesEvent {
+        final int maxCount;
+        final int actualCount;
+
+        public QualifiedMaxCountConstraintEvaluatedEvent(ValidationContext vCxt, Shape shape,
+                        Node focusNode, Constraint constraint, Path path, Set<Node> valueNodes,
+                        Set<Node> compareNodes, int maxCount, int actualCount, boolean valid) {
+            super(vCxt, shape, focusNode, constraint, path, valueNodes, compareNodes, valid);
+            this.maxCount = maxCount;
+            this.actualCount = actualCount;
+        }
+
+        public int getMaxCount() {
+            return maxCount;
+        }
+
+        public int getActualCount() {
+            return actualCount;
+        }
+    }
 }
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 d5f46d2a68..25b04fcb80 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,11 +18,6 @@
 
 package org.apache.jena.shacl.engine.constraint;
 
-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;
@@ -40,6 +35,7 @@ import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.lib.ShLib;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
 import org.apache.jena.sparql.core.PathBlock;
 import org.apache.jena.sparql.core.TriplePath;
 import org.apache.jena.sparql.core.Var;
@@ -53,6 +49,11 @@ import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransformCopyBase;
 import org.apache.jena.sparql.syntax.syntaxtransform.QueryTransformOps;
 import org.apache.jena.sparql.util.ModelUtils;
 
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /** The SPARQL validator algorithms. */
 /*package*/ class SparqlValidation {
 
@@ -134,12 +135,16 @@ import org.apache.jena.sparql.util.ModelUtils;
                     : substitute(violationTemplate, parameterMap, focusNode, path, valueNode);
                 vCxt.reportEntry(msg, shape, focusNode, path, valueNode, reportConstraint);
             }
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, reportConstraint, path, valueNode,
+                            b));
             return b;
         }
 
         ResultSet rs = qExec.execSelect();
-        if ( ! rs.hasNext() )
+        if ( ! rs.hasNext() ) {
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, reportConstraint, path, valueNode, true));
             return true;
+        }
 
         while(rs.hasNext()) {
             Binding row = rs.nextBinding();
@@ -163,6 +168,10 @@ import org.apache.jena.sparql.util.ModelUtils;
                 if ( qPath != null )
                     rPath = PathFactory.pathLink(qPath);
             }
+            final Path finalRPath = rPath;
+            final Node finalValue = value;
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, reportConstraint, finalRPath, finalValue,
+                            false));
             vCxt.reportEntry(msg, shape, focusNode, rPath, value, reportConstraint);
         }
         return false;
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/UniqueLangConstraint.java b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/UniqueLangConstraint.java
index dce350ace6..1e2bc3ae22 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/UniqueLangConstraint.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/UniqueLangConstraint.java
@@ -34,6 +34,8 @@ import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.ConstraintVisitor;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnPathNodesEvent;
+import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
 import org.apache.jena.shacl.vocabulary.SHACL;
 import org.apache.jena.sparql.path.Path;
 
@@ -65,19 +67,26 @@ public class UniqueLangConstraint implements Constraint {
             return;
         Set<String> results = new HashSet<>();
         Set<String> seen = new HashSet<>();
+        boolean passed = true;
         for ( Node obj : pathNodes) {
             if ( Util.isLangString(obj) ) {
                 String tag = obj.getLiteralLanguage().toLowerCase();
                 // Valid?
                 //LangTag.check(tag);
                 if ( seen.contains(tag) && ! results.contains(tag)) {
+                    passed = false;
                     String msg = toString()+" Duplicate langtag: "+obj.getLiteralLanguage();
+                    vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape,  focusNode, this, path, obj,
+                                    false));
                     vCxt.reportEntry(msg, shape, focusNode, path, null, this);
                     results.add(tag);
                 }
                 seen.add(tag);
             }
         }
+        if (passed){
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluatedOnPathNodesEvent(vCxt, shape,  focusNode, this, path, pathNodes,true));
+        }
     }
 
     @Override
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/HandlerBasedValidationListener.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/HandlerBasedValidationListener.java
new file mode 100644
index 0000000000..ad002ffbb5
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/HandlerBasedValidationListener.java
@@ -0,0 +1,233 @@
+/*
+ * 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.validation;
+
+import org.apache.jena.shacl.validation.event.EventUtil;
+import org.apache.jena.shacl.validation.event.ValidationEvent;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * ValidationListener implementation that allows for registering event handlers on a per-type basis.
+ * </p>
+ * <p>
+ * The handlers can be registered with any subclass or sub-interface of 'ValidationEvent', i.e., concrete
+ *  event classes their superclasses or interfaces.
+ *  </p>
+ * Example:
+ * <pre>
+ *  ValidationListener myListener =
+ *                         HandlerBasedValidationListener
+ *                                         .builder()
+ *                                         .forEventType(FocusNodeValidationStartedEvent.class)
+ *                                         .addSimpleHandler(e -> {
+ *                                              // ...
+ *                                         })
+ *                                         .forEventType(ConstraintEvaluatedEvent.class)
+ *                                         .addSimpleHandler(e -> {
+ *                                             // will be called for any subclass of ConstraintEvaluatedEvent
+ *                                         })
+ *                                         .build();
+ * </pre>
+ *
+ *
+ */
+public class HandlerBasedValidationListener implements ValidationListener {
+    private final Map<Class<? extends ValidationEvent>, List<Consumer<ValidationEvent>>> eventHandlers = new HashMap<>();
+    private final HandlerSelectionStrategy handlerSelectionStrategy;
+
+    private HandlerBasedValidationListener(HandlerSelectionStrategy handlerSelectionStrategy){
+        this.handlerSelectionStrategy = handlerSelectionStrategy;
+    }
+
+    public static Builder builder(HandlerSelectionStrategy handlerSelectionStrategy){
+        return new Builder(handlerSelectionStrategy);
+    }
+
+    public static Builder builder(){
+        return new Builder(new ClassHierarchyStrategy());
+    }
+
+    @Override public void onValidationEvent(ValidationEvent e) {
+        Objects.requireNonNull(e);
+        handlerSelectionStrategy.findHandlers(this.eventHandlers, e)
+                        .forEach(handler -> handler.accept(e));
+    }
+
+    private <T extends ValidationEvent> void registerHandlerInternal(Class<T> eventType, Consumer<? super T> handler) {
+        Objects.requireNonNull(eventType);
+        Objects.requireNonNull(handler);
+        eventHandlers.compute(eventType, (k, l) -> {
+            List<Consumer<ValidationEvent>> ret = Optional.ofNullable(l).orElse(new ArrayList<>());
+            //noinspection unchecked
+            ret.add((Consumer<ValidationEvent>) handler);
+            return ret;
+        });
+        handlerSelectionStrategy.onNewHandlerRegistered(eventType);
+    }
+
+    private static class FilteredEventHandler
+                    implements Consumer<ValidationEvent> {
+        private final Predicate<ValidationEvent> filter;
+        private final Consumer<ValidationEvent> handler;
+        public FilteredEventHandler(Predicate<ValidationEvent> filter, Consumer<ValidationEvent> handler) {
+            this.filter = filter;
+            this.handler = handler;
+        }
+
+        @Override public void accept(ValidationEvent validationEvent) {
+            if (filter.test(validationEvent)){
+                handler.accept(validationEvent);
+            }
+        }
+    }
+
+    public static class HandlerAdder<T extends ValidationEvent> {
+        private final Builder parent;
+        private final Class<T>[] eventTypes;
+
+        @SafeVarargs
+        public HandlerAdder(Builder parent, Class<T>... eventTypes) {
+            this.eventTypes = eventTypes;
+            this.parent = parent;
+        }
+
+        @SafeVarargs
+        public final Builder addSimpleHandlers(Consumer<? super T>... handlers) {
+            for(Consumer<? super T> handler:handlers) {
+                Builder ignoreMe = addSimpleHandler(handler);
+            }
+            return parent;
+        }
+
+        public Builder addSimpleHandler(Consumer<? super T> handler){
+            for(Class<T> eventType: eventTypes) {
+                parent.registerHandlerInternal(eventType, handler);
+            }
+            return parent;
+        }
+
+        public Builder addHandler(HandlerConfigurer<T> handlerConfigurer){
+            HandlerBuilder<T> hb = new HandlerBuilder<>();
+            handlerConfigurer.configure(hb);
+            for(Class<T> eventType: eventTypes) {
+                parent.registerHandlerInternal(eventType, hb.build());
+            }
+            return parent;
+        }
+    }
+
+    public static class Builder{
+        private HandlerBasedValidationListener listener;
+
+        private Builder(HandlerSelectionStrategy handlerSelectionStrategy) {
+            this.listener = new HandlerBasedValidationListener(handlerSelectionStrategy);
+        }
+
+        public <T extends ValidationEvent> HandlerAdder<T> forEventType(Class<T> eventType){
+            return new HandlerAdder<>(Builder.this, eventType);
+        }
+
+        @SafeVarargs
+        @SuppressWarnings("unchecked")
+        public final HandlerAdder<ValidationEvent> forEventTypes(Class<? extends ValidationEvent>... eventType){
+            return new HandlerAdder<>(Builder.this, (Class<ValidationEvent>[]) eventType);
+        }
+
+        public HandlerBasedValidationListener build(){
+            HandlerBasedValidationListener ret = listener;
+            listener = null;
+            return ret;
+        }
+
+        private <T extends ValidationEvent> void registerHandlerInternal(Class<T> eventType,
+                        Consumer<? super T> handler) {
+            listener.registerHandlerInternal(eventType, handler);
+        }
+    }
+
+    public interface HandlerSelectionStrategy {
+        Collection<Consumer<ValidationEvent>> findHandlers(Map<Class<? extends ValidationEvent>, List<Consumer<ValidationEvent>>> handlers, ValidationEvent event);
+
+        void onNewHandlerRegistered(Class<? extends ValidationEvent> eventType);
+    }
+
+    private static class ClassHierarchyStrategy implements HandlerSelectionStrategy  {
+        private final Map<Class<? extends ValidationEvent>, List<Consumer<ValidationEvent>>> registeredHandlersCache = new HashMap<>();
+        public ClassHierarchyStrategy() {
+        }
+
+        @Override public void onNewHandlerRegistered(Class<? extends ValidationEvent> eventType) {
+            registeredHandlersCache.clear();
+        }
+
+        @Override public Collection<Consumer<ValidationEvent>> findHandlers(
+                        Map<Class<? extends ValidationEvent>, List<Consumer<ValidationEvent>>> handlers,
+                        ValidationEvent event) {
+            return registeredHandlersCache.computeIfAbsent(event.getClass(), e -> {
+                List<Class<? extends ValidationEvent>> eventTypes =  EventUtil.getSuperclassesAndInterfaces(event.getClass()).collect(
+                                Collectors.toUnmodifiableList());
+                return getHandlers(handlers, eventTypes);
+            });
+        }
+
+        private List<Consumer<ValidationEvent>> getHandlers(Map<Class<? extends ValidationEvent>, List<Consumer<ValidationEvent>>> handlers, List<Class<? extends ValidationEvent>> eventTypes) {
+            return eventTypes.stream().flatMap(t -> handlers.getOrDefault(t, List.of()).stream()).collect(Collectors.toList());
+        }
+    }
+
+    public interface HandlerConditionCustomizer<T extends ValidationEvent> {
+        HandlerCustomizer<T> iff(Predicate<ValidationEvent> predicate);
+        void handle(Consumer<T> consumer);
+    }
+
+    public interface HandlerCustomizer<T extends ValidationEvent> {
+        void handle(Consumer<T> consumer);
+    }
+
+
+    public interface HandlerConfigurer<T extends ValidationEvent> {
+        void configure(HandlerConditionCustomizer<T> handlerCustomizer);
+    }
+
+    public static class HandlerBuilder<T extends ValidationEvent> implements HandlerCustomizer<T>, HandlerConditionCustomizer<T> {
+        private Predicate<ValidationEvent> predicate = null;
+        private Consumer<T> handler = null;
+        public HandlerBuilder<T> iff(Predicate<ValidationEvent> predicate){
+            this.predicate = predicate;
+            return this;
+        }
+        public void handle(Consumer<T> handler) {
+            this.handler = handler;
+        }
+        @SuppressWarnings("unchecked")
+        public Consumer<T> build(){
+            Objects.requireNonNull(handler);
+            if (predicate == null) {
+                return handler;
+            }
+            return (Consumer<T>) new FilteredEventHandler(predicate, (Consumer<ValidationEvent>) handler);
+        }
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/VLib.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/VLib.java
index ddf3cd6917..03c3e292da 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/VLib.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/VLib.java
@@ -34,6 +34,7 @@ import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.NodeShape;
 import org.apache.jena.shacl.parser.PropertyShape;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.*;
 import org.apache.jena.sparql.path.Path;
 
 /**
@@ -91,7 +92,7 @@ public class VLib {
             return;
         if ( vCxt.isVerbose() )
             out.println("S: "+shape);
-
+        vCxt.notifyValidationListener(() -> new FocusNodeValidationStartedEvent(vCxt, shape, focusNode));
         Path path;
         Set<Node> vNodes;
         if ( shape instanceof NodeShape ) {
@@ -101,6 +102,7 @@ public class VLib {
             PropertyShape propertyShape = (PropertyShape)shape;
             path = propertyShape.getPath();
             vNodes = ShaclPaths.valueNodes(data, focusNode, propertyShape.getPath());
+            vCxt.notifyValidationListener(() -> new ValueNodesDeterminedForPropertyShapeEvent(vCxt, shape, focusNode, path, vNodes));
         } else {
             if ( vCxt.isVerbose() )
                 out.println("Z: "+shape);
@@ -119,6 +121,7 @@ public class VLib {
         validationPropertyShapes(vCxt, data, shape.getPropertyShapes(), focusNode);
         if ( vCxt.isVerbose() )
             out.println();
+        vCxt.notifyValidationListener(() -> new FocusNodeValidationFinishedEvent(vCxt, shape, focusNode));
     }
 
     static void validationPropertyShapes(ValidationContext vCxt, Graph data, Collection<PropertyShape> propertyShapes, Node focusNode) {
@@ -137,10 +140,10 @@ public class VLib {
             return;
         if ( vCxt.isVerbose() )
             out.println("P: "+propertyShape);
-
+        vCxt.notifyValidationListener(() -> new FocusNodeValidationStartedEvent(vCxt, propertyShape, focusNode));
         Path path = propertyShape.getPath();
         Set<Node> vNodes = ShaclPaths.valueNodes(data, focusNode, path);
-
+        vCxt.notifyValidationListener(() -> new ValueNodesDeterminedForPropertyShapeEvent(vCxt, propertyShape, focusNode, path, vNodes));
         for ( Constraint c : propertyShape.getConstraints() ) {
             if ( vCxt.isVerbose() )
                 out.println("C: "+focusNode+" :: "+c);
@@ -150,6 +153,7 @@ public class VLib {
         vNodes.forEach(vNode->{
             validationPropertyShapes(vCxt, data, propertyShape.getPropertyShapes(), vNode);
         });
+        vCxt.notifyValidationListener(() -> new FocusNodeValidationFinishedEvent(vCxt, propertyShape, focusNode));
     }
 
     // ValidationProc
@@ -195,12 +199,16 @@ public class VLib {
         if ( path == null ) {
             if ( pathNodes != null )
                 throw new InternalErrorException("Path is null but pathNodes is not null");
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluationForNodeShapeStartedEvent(vCxt, shape, focusNode, c));
             c.validateNodeShape(vCxt, data, shape, focusNode);
+            vCxt.notifyValidationListener(() -> new ConstraintEvaluationForNodeShapeFinishedEvent(vCxt, shape, focusNode,c));
             return;
         }
         if ( pathNodes == null )
             throw new InternalErrorException("Path is not null but pathNodes is null");
+        vCxt.notifyValidationListener(() -> new ConstraintEvaluationForPropertyShapeStartedEvent(vCxt, shape, focusNode, c,  path, pathNodes));
         c.validatePropertyShape(vCxt, data, shape, focusNode, path, pathNodes);
+        vCxt.notifyValidationListener(() -> new ConstraintEvaluationForPropertyShapeFinishedEvent(vCxt, shape, focusNode, c, path, pathNodes));
     }
 
 }
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationListener.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationListener.java
index 3ced5f8efa..e1bb3f1b43 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/ValidationListener.java
@@ -16,23 +16,15 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.validation.event.*;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+/**
+ * Callback invoked at various points during the validation process.
+ */
+public interface ValidationListener {
+
+    void onValidationEvent(ValidationEvent e);
 
-public class TC_SHACL { }
+}
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 44191a8c43..ea78fa96a7 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
@@ -28,6 +28,7 @@ import org.apache.jena.shacl.Shapes;
 import org.apache.jena.shacl.ValidationReport;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.event.*;
 
 public class ValidationProc {
     /* 3.4 Validation
@@ -84,10 +85,16 @@ public class ValidationProc {
     }
 
     private static ValidationReport plainValidation(ValidationContext vCxt, Shapes shapes, Graph data) {
-        shapes.getTargetShapes().forEach(shape->plainValidation(vCxt, shape, data));
-        if ( vCxt.isVerbose() )
-            out.ensureStartOfLine();
-        return vCxt.generateReport();
+        Collection<Shape> targetShapes = shapes.getTargetShapes();
+        vCxt.notifyValidationListener(() -> new TargetShapesValidationStartedEvent(vCxt, targetShapes));
+        try {
+            targetShapes.forEach(shape->plainValidation(vCxt, shape, data));
+            if (vCxt.isVerbose())
+                out.ensureStartOfLine();
+            return vCxt.generateReport();
+        } finally {
+            vCxt.notifyValidationListener(() -> new TargetShapesValidationFinishedEvent(vCxt, targetShapes));
+        }
     }
 
     private static void plainValidation(ValidationContext vCxt, Shape shape, Graph data) {
@@ -105,12 +112,18 @@ public class ValidationProc {
     }
 
     private static ValidationReport plainValidationNode(ValidationContext vCxt, Shapes shapes, Node node, Graph data) {
-        shapes.getTargetShapes().forEach(shape->
-            plainValidationNode(vCxt, data, node, shape)
+        Collection<Shape> targetShapes = shapes.getTargetShapes();
+        vCxt.notifyValidationListener(() -> new TargetShapesValidationStartedEvent(vCxt, targetShapes));
+        try {
+            targetShapes.forEach(shape ->
+                            plainValidationNode(vCxt, data, node, shape)
             );
-        if ( vCxt.isVerbose() )
-            out.ensureStartOfLine();
-        return vCxt.generateReport();
+            if (vCxt.isVerbose())
+                out.ensureStartOfLine();
+            return vCxt.generateReport();
+        } finally {
+            vCxt.notifyValidationListener(() -> new TargetShapesValidationFinishedEvent(vCxt, targetShapes));
+        }
     }
 
     private static void plainValidationNode(ValidationContext vCxt, Graph data, Node node, Shape shape) {
@@ -126,7 +139,7 @@ public class ValidationProc {
      */
     private static void plainValidationInternal(ValidationContext vCxt, Graph data, Node node, Shape shape) {
         Collection<Node> focusNodes;
-
+        vCxt.notifyValidationListener(() -> new ShapeValidationStartedEvent(vCxt, shape));
         if ( node != null ) {
             if (! VLib.isFocusNode(shape, node, data))
                 return ;
@@ -134,6 +147,7 @@ public class ValidationProc {
         } else {
             focusNodes = VLib.focusNodes(data, shape);
         }
+        vCxt.notifyValidationListener(() -> new FocusNodesDeterminedEvent(vCxt, shape, focusNodes));
 
         if ( vCxt.isVerbose() ) {
             out.println(shape.toString());
@@ -149,11 +163,17 @@ public class ValidationProc {
         if ( vCxt.isVerbose() ) {
             out.decIndent();
         }
+        vCxt.notifyValidationListener(() -> new ShapeValidationFinishedEvent(vCxt, shape));
     }
 
     // Make ValidationContext carry the ShaclValidator to recurse on.
     // Recursion for shapes of shapes. "shape-expecting constraint parameters"
     public static void execValidateShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
-        VLib.validateShape(vCxt, data, shape, focusNode);
+        vCxt.notifyValidationListener(() -> new ShapeValidationStartedEvent(vCxt, shape));
+        try {
+            VLib.validateShape(vCxt, data, shape, focusNode);
+        } finally {
+            vCxt.notifyValidationListener(() -> new ShapeValidationFinishedEvent(vCxt, shape));
+        }
     }
 }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationEvent.java
new file mode 100644
index 0000000000..a8941218a0
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationEvent.java
@@ -0,0 +1,60 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+
+/**
+ * Constraint-related event.
+ */
+public abstract class AbstractConstraintEvaluationEvent extends AbstractFocusNodeValidationEvent implements
+                ConstraintEvaluationEvent {
+    protected final Constraint constraint;
+
+    public AbstractConstraintEvaluationEvent(ValidationContext vCxt, Shape shape, Node focusNode,
+                    Constraint constraint) {
+        super(vCxt, shape, focusNode);
+        this.constraint = constraint;
+    }
+
+    @Override public Constraint getConstraint() {
+        return constraint;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractConstraintEvaluationEvent that = (AbstractConstraintEvaluationEvent) o;
+        return getConstraint().equals(that.getConstraint());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getConstraint().hashCode();
+        return result;
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationForPathEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationForPathEvent.java
new file mode 100644
index 0000000000..3a79e8e790
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationForPathEvent.java
@@ -0,0 +1,62 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+/**
+ * Constraint-related event that pertains to a path.
+ */
+public abstract class AbstractConstraintEvaluationForPathEvent extends AbstractConstraintEvaluationEvent implements
+                ConstraintEvaluationForPathEvent {
+    protected final Path path;
+
+    public AbstractConstraintEvaluationForPathEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, Path path) {
+        super(vCxt, shape, focusNode, constraint);
+        this.path = path;
+    }
+
+    @Override public Path getPath() {
+        return path;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractConstraintEvaluationForPathEvent that = (AbstractConstraintEvaluationForPathEvent) o;
+        return getPath().equals(that.getPath());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getPath().hashCode();
+        return result;
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationOnPathNodesEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationOnPathNodesEvent.java
new file mode 100644
index 0000000000..f0ed455deb
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationOnPathNodesEvent.java
@@ -0,0 +1,64 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+import java.util.Set;
+
+/**
+ * Event that pertains to constraint evaluation on multiple value nodes.
+ */
+public abstract class AbstractConstraintEvaluationOnPathNodesEvent extends AbstractConstraintEvaluationForPathEvent
+                implements
+                ConstraintEvaluationOnPathNodesEvent {
+    protected final ImmutableLazySetCopy<Node> valueNodes;
+
+    public AbstractConstraintEvaluationOnPathNodesEvent(ValidationContext vCxt, Shape shape, Node focusNode,
+                    Constraint constraint, Path path, Set<Node> valueNodes) {
+        super(vCxt, shape, focusNode, constraint, path);
+        this.valueNodes = new ImmutableLazySetCopy<>(valueNodes);
+    }
+
+    @Override public Set<Node> getValueNodes() {
+        return valueNodes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractConstraintEvaluationOnPathNodesEvent that = (AbstractConstraintEvaluationOnPathNodesEvent) o;
+        return getValueNodes().equals(that.getValueNodes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getValueNodes().hashCode();
+        return result;
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationOnSinglePathNodeEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationOnSinglePathNodeEvent.java
new file mode 100644
index 0000000000..82f5f56b41
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractConstraintEvaluationOnSinglePathNodeEvent.java
@@ -0,0 +1,62 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+/**
+ * Event resulting from constraint evaluation on a single value node.
+ */
+public abstract class AbstractConstraintEvaluationOnSinglePathNodeEvent extends AbstractConstraintEvaluationForPathEvent
+                implements
+                ConstraintEvaluationOnSinglePathNodeEvent {
+    protected final Node valueNode;
+
+    public AbstractConstraintEvaluationOnSinglePathNodeEvent(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Constraint constraint, Path path, Node valueNode) {
+        super(vCxt, shape, focusNode, constraint, path);
+        this.valueNode = valueNode;
+    }
+
+    @Override public Node getValueNode() {
+        return valueNode;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractConstraintEvaluationOnSinglePathNodeEvent that = (AbstractConstraintEvaluationOnSinglePathNodeEvent) o;
+        return getValueNode().equals(that.getValueNode());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getValueNode().hashCode();
+        return result;
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractFocusNodeValidationEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractFocusNodeValidationEvent.java
new file mode 100644
index 0000000000..d343344e83
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractFocusNodeValidationEvent.java
@@ -0,0 +1,62 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Objects;
+
+/**
+ * Event type used for all events after the selection of a focus node.
+ */
+public abstract class AbstractFocusNodeValidationEvent extends AbstractShapeValidationEvent implements
+                FocusNodeValidationEvent {
+    protected final Node focusNode;
+
+    public AbstractFocusNodeValidationEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode) {
+        super(vCxt, shape);
+        Objects.requireNonNull(focusNode);
+        this.focusNode = focusNode;
+    }
+
+    @Override public Node getFocusNode() {
+        return focusNode;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractFocusNodeValidationEvent that = (AbstractFocusNodeValidationEvent) o;
+        return getFocusNode().equals(that.getFocusNode());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getFocusNode().hashCode();
+        return result;
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractShapeValidationEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractShapeValidationEvent.java
new file mode 100644
index 0000000000..ed8efec253
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractShapeValidationEvent.java
@@ -0,0 +1,60 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Objects;
+
+/**
+ * Event type used for all events after the selection of a shape to validate against.
+ */
+public abstract class AbstractShapeValidationEvent extends AbstractValidationEvent implements ShapeValidationEvent {
+    protected final Shape shape;
+
+    public AbstractShapeValidationEvent(ValidationContext vCxt,
+                    Shape shape) {
+        super(vCxt);
+        Objects.requireNonNull(shape);
+        this.shape = shape;
+    }
+
+    @Override public Shape getShape() {
+        return shape;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractShapeValidationEvent that = (AbstractShapeValidationEvent) o;
+        return getShape().equals(that.getShape());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getShape().hashCode();
+        return result;
+    }
+
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractTargetShapesValidationEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractTargetShapesValidationEvent.java
new file mode 100644
index 0000000000..ad9eb7db9a
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractTargetShapesValidationEvent.java
@@ -0,0 +1,58 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Collection;
+
+/**
+ * Type of events that report the set of target shapes used during validation.
+ */
+public abstract class AbstractTargetShapesValidationEvent extends AbstractValidationEvent implements TargetShapeValidationEvent {
+    protected final ImmutableLazyCollectionCopy<Shape> targetShapes;
+
+    public AbstractTargetShapesValidationEvent(ValidationContext vCxt,
+                    Collection<Shape> targetShapes) {
+        super(vCxt);
+        this.targetShapes = new ImmutableLazyCollectionCopy<>(targetShapes);
+    }
+
+    @Override public Collection<Shape> getTargetShapes() {
+        return targetShapes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        AbstractTargetShapesValidationEvent that = (AbstractTargetShapesValidationEvent) o;
+        return getTargetShapes().equals(that.getTargetShapes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getTargetShapes().hashCode();
+        return result;
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractValidationEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractValidationEvent.java
new file mode 100644
index 0000000000..a129414f7e
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/AbstractValidationEvent.java
@@ -0,0 +1,52 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.shacl.engine.ValidationContext;
+
+import java.util.Objects;
+
+/**
+ * Abstract superclass for all events, providing access to the {@link ValidationContext}.
+ */
+public abstract class AbstractValidationEvent implements ValidationEvent {
+    protected final ValidationContext vCxt;
+
+    public AbstractValidationEvent(ValidationContext vCxt) {
+        this.vCxt = vCxt;
+        Objects.requireNonNull(vCxt);
+    }
+
+    @Override public ValidationContext getValidationContext() {
+        return vCxt;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        AbstractValidationEvent that = (AbstractValidationEvent) o;
+        return vCxt.equals(that.vCxt);
+    }
+
+    @Override public int hashCode() {
+        return vCxt.hashCode();
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/CompareNodesEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/CompareNodesEvent.java
index 3ced5f8efa..b6a7985feb 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/CompareNodesEvent.java
@@ -16,23 +16,16 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+import java.util.Set;
 
-public class TC_SHACL { }
+/**
+ * Base class for events resulting from evaluating constraints that compare value nodes with other nodes, such
+ * as <code>sh:equals</code> or <code>sh:lessThan</code>.
+ */
+public interface CompareNodesEvent extends ConstraintEvaluationEvent {
+    Set<Node> getCompareNodes();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedEvent.java
index 3ced5f8efa..0cb38fd928 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedEvent.java
@@ -16,23 +16,17 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+import java.util.Set;
 
-public class TC_SHACL { }
+/**
+ * Type of events resulting from evaluating a constraint on a Set of value nodes or, in the case of constraints of a
+ * node shape, on a single value node.
+ */
+public interface ConstraintEvaluatedEvent extends ConstraintEvaluationEvent {
+    boolean isValid();
+    Set<Node> getValueNodes();
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeEvent.java
new file mode 100644
index 0000000000..d62c5d7ee8
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeEvent.java
@@ -0,0 +1,76 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Event type resulting from constraint evaluation on the focus node (i.e. in a node shape).
+ */
+public class ConstraintEvaluatedOnFocusNodeEvent extends AbstractConstraintEvaluationEvent
+                implements ConstraintEvaluatedEvent {
+    protected final boolean valid;
+
+    public ConstraintEvaluatedOnFocusNodeEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, boolean valid) {
+        super(vCxt, shape, focusNode, constraint);
+        this.valid = valid;
+    }
+
+    @Override public boolean isValid() {
+        return valid;
+    }
+
+    @Override public Set<Node> getValueNodes() {
+        return Collections.singleton(getFocusNode());
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnFocusNodeEvent that = (ConstraintEvaluatedOnFocusNodeEvent) o;
+        return isValid() == that.isValid();
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (isValid() ? 1 : 0);
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnFocusNodeEvent{" +
+                        "constraint=" + constraint +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent.java
new file mode 100644
index 0000000000..4c27cf03ec
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent.java
@@ -0,0 +1,72 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Set;
+
+/**
+ * Event resulting from evaluating a constraint on the focus node, using compare nodes.
+ */
+public class ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent extends ConstraintEvaluatedOnFocusNodeEvent
+                implements CompareNodesEvent {
+    protected final ImmutableLazySetCopy<Node> compareNodes;
+
+    public ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, Set<Node> compareNodes, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, valid);
+        this.compareNodes = new ImmutableLazySetCopy<>(compareNodes);
+    }
+
+    @Override public Set<Node> getCompareNodes() {
+        return compareNodes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent that = (ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent) o;
+        return getCompareNodes().equals(that.getCompareNodes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getCompareNodes().hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnFocusNodeWithCompareNodesEvent{" +
+                        "constraint=" + constraint +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        ", compareNodes=" + compareNodes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent.java
new file mode 100644
index 0000000000..b9107782ef
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent.java
@@ -0,0 +1,73 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Event resulting from evaluating a constraint on the focus node, using a single compare node.
+ */
+public class ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent extends ConstraintEvaluatedOnFocusNodeEvent
+                implements CompareNodesEvent {
+    protected final Node compareNode;
+
+    public ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, Node compareNode, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, valid);
+        this.compareNode = compareNode;
+    }
+
+    @Override public Set<Node> getCompareNodes() {
+        return Collections.singleton(compareNode);
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent that = (ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent) o;
+        return compareNode.equals(that.compareNode);
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + compareNode.hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnFocusNodeWithSingleCompareNodeEvent{" +
+                        "constraint=" + constraint +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        ", compareNode=" + compareNode +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnPathNodesEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnPathNodesEvent.java
new file mode 100644
index 0000000000..b6edea632d
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnPathNodesEvent.java
@@ -0,0 +1,75 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+import java.util.Set;
+
+/**
+ * Event emitted when a constraint is evaluated on multiple value nodes.
+ */
+public class ConstraintEvaluatedOnPathNodesEvent extends AbstractConstraintEvaluationOnPathNodesEvent
+                implements ConstraintEvaluatedEvent {
+    protected final boolean valid;
+
+    public ConstraintEvaluatedOnPathNodesEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, Path path,
+                    Set<Node> valueNodes, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, path, valueNodes);
+        this.valid = valid;
+    }
+
+    @Override public boolean isValid() {
+        return valid;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnPathNodesEvent that = (ConstraintEvaluatedOnPathNodesEvent) o;
+        return isValid() == that.isValid();
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (isValid() ? 1 : 0);
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnPathNodesEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNodes=" + valueNodes +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnPathNodesWithCompareNodesEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnPathNodesWithCompareNodesEvent.java
new file mode 100644
index 0000000000..7a99ec86f8
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnPathNodesWithCompareNodesEvent.java
@@ -0,0 +1,76 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+import java.util.Set;
+
+/**
+ * Event emitted when a constraint is evaluated on multiple value nodes and multiple compare nodes.
+ */
+public class ConstraintEvaluatedOnPathNodesWithCompareNodesEvent extends ConstraintEvaluatedOnPathNodesEvent
+                implements CompareNodesEvent {
+    protected final ImmutableLazySetCopy<Node> compareNodes;
+
+    public ConstraintEvaluatedOnPathNodesWithCompareNodesEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, Path path,
+                    Set<Node> valueNodes, Set<Node> compareNodes, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, path, valueNodes, valid);
+        this.compareNodes = new ImmutableLazySetCopy<>(compareNodes);
+    }
+
+    public Set<Node> getCompareNodes() {
+        return compareNodes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnPathNodesWithCompareNodesEvent that = (ConstraintEvaluatedOnPathNodesWithCompareNodesEvent) o;
+        return getCompareNodes().equals(that.getCompareNodes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getCompareNodes().hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnPathNodesWithCompareNodesEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNodes=" + valueNodes +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        ", compareNodes=" + compareNodes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeEvent.java
new file mode 100644
index 0000000000..f73d56fc3d
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeEvent.java
@@ -0,0 +1,75 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class ConstraintEvaluatedOnSinglePathNodeEvent extends AbstractConstraintEvaluationOnSinglePathNodeEvent
+                implements ConstraintEvaluatedEvent {
+    protected final boolean valid;
+
+    public ConstraintEvaluatedOnSinglePathNodeEvent(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Constraint constraint, Path path, Node valueNode, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, path, valueNode);
+        this.valid = valid;
+    }
+
+    @Override public boolean isValid() {
+        return valid;
+    }
+
+    @Override public Set<Node> getValueNodes() {
+        return Collections.singleton(getValueNode());
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnSinglePathNodeEvent that = (ConstraintEvaluatedOnSinglePathNodeEvent) o;
+        return isValid() == that.isValid();
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (isValid() ? 1 : 0);
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnSinglePathNodeEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNode=" + valueNode +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent.java
new file mode 100644
index 0000000000..e599359855
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent.java
@@ -0,0 +1,79 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+import java.util.Set;
+
+/**
+ * Event emitted when a constraint is evaluated on a single value node
+ * using a set of compare nodes. For example, when
+ * a {@link org.apache.jena.shacl.engine.constraint.DisjointConstraint DisjointConstraint}
+ * is evaluated.
+ */
+public class ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent extends ConstraintEvaluatedOnSinglePathNodeEvent
+                implements CompareNodesEvent {
+    protected final ImmutableLazySetCopy<Node> compareNodes;
+
+    public ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode,
+                    Constraint constraint, Path path,
+                    Node valueNode, Set<Node> compareNodes, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, path, valueNode, valid);
+        this.compareNodes = new ImmutableLazySetCopy<>(compareNodes);
+    }
+
+    @Override public Set<Node> getCompareNodes() {
+        return compareNodes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent that = (ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent) o;
+        return getCompareNodes().equals(that.getCompareNodes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getCompareNodes().hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnSinglePathNodeWithCompareNodesEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNode=" + valueNode +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        ", compareNodes=" + compareNodes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent.java
new file mode 100644
index 0000000000..2ae2899d9f
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent.java
@@ -0,0 +1,76 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+/**
+ * Event emitted when a constraint is evaluated on a single value node
+ * with a single compare node. For example, when a
+ * {@link org.apache.jena.shacl.engine.constraint.LessThanConstraint LessThanConstraint} is evaluated.
+ */
+public class ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent
+                extends ConstraintEvaluatedOnSinglePathNodeEvent implements SingleCompareNodeEvent {
+    protected final Node compareNode;
+
+    public ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent(
+                    ValidationContext vCxt, Shape shape,
+                    Node focusNode, Constraint constraint, Path path,
+                    Node valueNode, Node compareNode, boolean valid) {
+        super(vCxt, shape, focusNode, constraint, path, valueNode, valid);
+        this.compareNode = compareNode;
+    }
+
+    @Override public Node getCompareNode() {
+        return compareNode;
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent that = (ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent) o;
+        return getCompareNode().equals(that.getCompareNode());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getCompareNode().hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluatedOnSinglePathNodeWithSingleCompareNodeEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNode=" + valueNode +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", valid=" + valid +
+                        ", compareNode=" + compareNode +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationEvent.java
index 3ced5f8efa..3722e13702 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationEvent.java
@@ -16,23 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.parser.Constraint;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+public interface ConstraintEvaluationEvent extends FocusNodeValidationEvent {
+    Constraint getConstraint();
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForNodeShapeFinishedEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForNodeShapeFinishedEvent.java
new file mode 100644
index 0000000000..a8e6edc926
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForNodeShapeFinishedEvent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+
+/**
+ * Event emitted when the validation of a node shape is finished.
+ */
+public class ConstraintEvaluationForNodeShapeFinishedEvent extends AbstractConstraintEvaluationEvent
+                implements ValidationLifecycleEvent {
+    public ConstraintEvaluationForNodeShapeFinishedEvent(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Constraint constraint) {
+        super(vCxt, shape, focusNode, constraint);
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluationForNodeShapeFinishedEvent{" +
+                        "constraint=" + constraint +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForNodeShapeStartedEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForNodeShapeStartedEvent.java
new file mode 100644
index 0000000000..d64ad8388a
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForNodeShapeStartedEvent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Constraint;
+import org.apache.jena.shacl.parser.Shape;
+
+/**
+ * Event emitted when the validation of a node shape has begun.
+ */
+public class ConstraintEvaluationForNodeShapeStartedEvent extends AbstractConstraintEvaluationEvent
+                implements ValidationLifecycleEvent {
+    public ConstraintEvaluationForNodeShapeStartedEvent(ValidationContext vCxt, Shape shape, Node focusNode,
+                    Constraint constraint) {
+        super(vCxt, shape, focusNode, constraint);
+    }
+
+    @Override public String toString() {
+        return "ConstraintEvaluationForNodeShapeStartedEvent{" +
+                        "constraint=" + constraint +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPathEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPathEvent.java
index 3ced5f8efa..725a699a97 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPathEvent.java
@@ -16,23 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.sparql.path.Path;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+public interface ConstraintEvaluationForPathEvent extends ConstraintEvaluationEvent {
+    Path getPath();
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPropertyShapeFinishedEvent.java
similarity index 52%
copy from jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPropertyShapeFinishedEvent.java
index e8715a32c2..2975a4afed 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPropertyShapeFinishedEvent.java
@@ -16,38 +16,34 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl.engine.constraint;
+package org.apache.jena.shacl.validation.event;
 
-import java.util.Set;
-
-import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
-import org.apache.jena.shacl.validation.ReportItem;
 import org.apache.jena.sparql.path.Path;
 
-/** A constraint that combines other constraints */
-public abstract class ConstraintOp implements Constraint {
+import java.util.Set;
 
-    @Override
-    final
-    public void validateNodeShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
-        ReportItem item = validate(vCxt, data, focusNode);
-        if ( item != null )
-            vCxt.reportEntry(item, shape, focusNode, null, this);
+/**
+ * Event emitted when a property shape has been evaluated completely.
+ */
+public class ConstraintEvaluationForPropertyShapeFinishedEvent extends AbstractConstraintEvaluationOnPathNodesEvent
+                implements ValidationLifecycleEvent {
+    public ConstraintEvaluationForPropertyShapeFinishedEvent(ValidationContext vCxt, Shape shape,
+                    Node focusNode, Constraint constraint, Path path,
+                    Set<Node> valueNodes) {
+        super(vCxt, shape, focusNode, constraint, path, valueNodes);
     }
 
-    @Override
-    final
-    public void validatePropertyShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode, Path path, Set<Node> pathNodes) {
-        pathNodes.forEach(n-> {
-            ReportItem item = validate(vCxt, data, n);
-            if ( item != null )
-                vCxt.reportEntry(item, shape, focusNode, path, this);
-        });
+    @Override public String toString() {
+        return "ConstraintEvaluationForPropertyShapeFinishedEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNodes=" + valueNodes +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        '}';
     }
-
-    public abstract ReportItem validate(ValidationContext vCxt, Graph data, Node node);
 }
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPropertyShapeStartedEvent.java
similarity index 52%
copy from jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPropertyShapeStartedEvent.java
index e8715a32c2..b425aa42ec 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/engine/constraint/ConstraintOp.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationForPropertyShapeStartedEvent.java
@@ -16,38 +16,34 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl.engine.constraint;
+package org.apache.jena.shacl.validation.event;
 
-import java.util.Set;
-
-import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.shacl.engine.ValidationContext;
 import org.apache.jena.shacl.parser.Constraint;
 import org.apache.jena.shacl.parser.Shape;
-import org.apache.jena.shacl.validation.ReportItem;
 import org.apache.jena.sparql.path.Path;
 
-/** A constraint that combines other constraints */
-public abstract class ConstraintOp implements Constraint {
+import java.util.Set;
 
-    @Override
-    final
-    public void validateNodeShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode) {
-        ReportItem item = validate(vCxt, data, focusNode);
-        if ( item != null )
-            vCxt.reportEntry(item, shape, focusNode, null, this);
+/**
+ * Event emitted when the validation of a shape has begun.
+ */
+public class ConstraintEvaluationForPropertyShapeStartedEvent extends AbstractConstraintEvaluationOnPathNodesEvent
+                implements ValidationLifecycleEvent {
+    public ConstraintEvaluationForPropertyShapeStartedEvent(ValidationContext vCxt, Shape shape, Node focusNode,
+                    Constraint constraint, Path path,
+                    Set<Node> pathNodes) {
+        super(vCxt, shape, focusNode, constraint, path, pathNodes);
     }
 
-    @Override
-    final
-    public void validatePropertyShape(ValidationContext vCxt, Graph data, Shape shape, Node focusNode, Path path, Set<Node> pathNodes) {
-        pathNodes.forEach(n-> {
-            ReportItem item = validate(vCxt, data, n);
-            if ( item != null )
-                vCxt.reportEntry(item, shape, focusNode, path, this);
-        });
+    @Override public String toString() {
+        return "ConstraintEvaluationForPropertyShapeStartedEvent{" +
+                        "constraint=" + constraint +
+                        ", path=" + path +
+                        ", valueNodes=" + valueNodes +
+                        ", focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        '}';
     }
-
-    public abstract ReportItem validate(ValidationContext vCxt, Graph data, Node node);
 }
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationOnPathNodesEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationOnPathNodesEvent.java
index 3ced5f8efa..1fece7d97d 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationOnPathNodesEvent.java
@@ -16,23 +16,12 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+import java.util.Set;
 
-public class TC_SHACL { }
+public interface ConstraintEvaluationOnPathNodesEvent extends ConstraintEvaluationForPathEvent {
+    Set<Node> getValueNodes();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationOnSinglePathNodeEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationOnSinglePathNodeEvent.java
index 3ced5f8efa..b95d3a05f8 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ConstraintEvaluationOnSinglePathNodeEvent.java
@@ -16,23 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+public interface ConstraintEvaluationOnSinglePathNodeEvent extends ConstraintEvaluationForPathEvent {
+    Node getValueNode();
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/EventPredicates.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/EventPredicates.java
new file mode 100644
index 0000000000..0e00e262cf
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/EventPredicates.java
@@ -0,0 +1,105 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.impl.LiteralLabel;
+import org.apache.jena.shacl.engine.constraint.DatatypeConstraint;
+import org.apache.jena.shacl.parser.Constraint;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+
+public abstract class EventPredicates {
+    public static Predicate<ValidationEvent> isOfType(Class<? extends ValidationEvent> type) {
+        return e -> type.equals(e.getClass());
+    }
+
+    public static Predicate<ValidationEvent> isOfTypeOrSubtype(Class<? extends ValidationEvent> type) {
+        return e -> EventUtil.getSuperclassesAndInterfaces(e.getClass()).anyMatch(s -> s.equals(type));
+    }
+
+    public static <E extends ValidationEvent> Predicate<ValidationEvent> testIfType(Class<E> type, Predicate<E> predicate, boolean defaultValue){
+        return e -> testIfType(e, type, predicate, defaultValue);
+    }
+
+    public static <E extends ValidationEvent> Predicate<ValidationEvent> testIfTypeElseFalse(Class<E> type, Predicate<E> predicate){
+        return e -> testIfType(e, type, predicate, false);
+    }
+
+    private static <E extends ValidationEvent> boolean testIfType(ValidationEvent e, Class<E> type, Predicate<E> predicate, boolean defaultValue){
+        if (type.isAssignableFrom(e.getClass())) {
+            return predicate.test(type.cast(e));
+        }
+        return defaultValue;
+    }
+
+    public static NodePredicate<ShapeValidationEvent> shapeNode(){
+        return new NodePredicate<>(ShapeValidationEvent.class, e -> e.getShape().getShapeNode());
+    }
+
+    public static NodePredicate<FocusNodeValidationEvent> focusNode(){
+        return new NodePredicate<>(FocusNodeValidationEvent.class, FocusNodeValidationEvent::getFocusNode);
+    }
+
+    public static Predicate<ValidationEvent> hasConstraintOfType(Class<? extends Constraint> constraintType) {
+        Objects.requireNonNull(constraintType);
+        return testIfType(ConstraintEvaluationEvent.class, e -> constraintType.isAssignableFrom(e.getConstraint().getClass()), false);
+    }
+
+    public static Predicate<ValidationEvent> hasDatatypeConstraint(){
+        return hasConstraintOfType(DatatypeConstraint.class);
+    }
+
+    public static Predicate<ValidationEvent> isValid(){
+        return testIfTypeElseFalse(ConstraintEvaluatedEvent.class, ConstraintEvaluatedEvent::isValid);
+    }
+
+    public static class NodePredicate<E extends ValidationEvent> {
+        private final Function<ValidationEvent, Node> nodeAccessor;
+
+        @SuppressWarnings("unchecked")
+        public NodePredicate(Class<E> type, Function<E, Node> nodeAccessor) {
+            this.nodeAccessor = e -> type.isAssignableFrom(e.getClass()) ?  nodeAccessor.apply((E) e) : null;
+        }
+
+        public Predicate<ValidationEvent> makePredicate(Predicate<Node> predicate){
+            return e -> Optional.ofNullable(nodeAccessor.apply(e)).map(predicate::test).orElse(false);
+        }
+
+        public Predicate<ValidationEvent> isBlank(){
+            return makePredicate(Node::isBlank);
+        }
+
+        public Predicate<ValidationEvent> isLiteral(){
+            return makePredicate(Node::isLiteral);
+        }
+
+        public Predicate<ValidationEvent> uriEquals(String uri){
+            return makePredicate( n -> n.isURI() && n.getURI().equals(uri));
+        }
+
+        public Predicate<ValidationEvent> literalEquals(LiteralLabel literalLabel) {
+            return makePredicate( n -> n.isLiteral() && n.getLiteral().sameValueAs(literalLabel));
+        }
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/EventUtil.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/EventUtil.java
new file mode 100644
index 0000000000..cffd05827f
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/EventUtil.java
@@ -0,0 +1,49 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+public abstract class EventUtil {
+    @SuppressWarnings("unchecked") public static Stream<Class<? extends ValidationEvent>> getSuperclassesAndInterfaces(
+                    Class<? extends ValidationEvent> eventType) {
+        Stream<Class<? extends ValidationEvent>> superInterfaces = Arrays.stream(eventType.getInterfaces())
+                        .filter(ValidationEvent.class::isAssignableFrom)
+                        .flatMap(iface -> getSuperclassesAndInterfaces((Class<? extends ValidationEvent>) iface));
+         Class<?> superType =  eventType.getSuperclass();
+        Stream<Class<? extends ValidationEvent>> superclasses;
+         if (superType != null && ValidationEvent.class.isAssignableFrom(superType)){
+             superclasses = getSuperclassesAndInterfaces(
+                            (Class<? extends ValidationEvent>) superType);
+         } else {
+             superclasses = Stream.empty();
+         }
+         return Stream.concat(superInterfaces, Stream.concat(superclasses, Stream.of(eventType)));
+    }
+
+    public static boolean nodeUriEquals(Node node, String uri) {
+        if (node.isURI()) {
+            return node.getURI().equals(uri);
+        }
+        return false;
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationEvent.java
index 3ced5f8efa..3fdde6ee7c 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationEvent.java
@@ -16,23 +16,13 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+/**
+ * Interface for events that are specific to a given focus node.
+ */
+public interface FocusNodeValidationEvent extends ShapeValidationEvent {
+    Node getFocusNode();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationFinishedEvent.java
similarity index 51%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationFinishedEvent.java
index 3ced5f8efa..7b87bdcdcd 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationFinishedEvent.java
@@ -16,23 +16,25 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+/**
+ * Event emitted when the validation of a focus node is finished.
+ */
+public class FocusNodeValidationFinishedEvent extends AbstractFocusNodeValidationEvent implements ValidationLifecycleEvent {
+    public FocusNodeValidationFinishedEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode) {
+        super(vCxt, shape, focusNode);
+    }
 
-public class TC_SHACL { }
+    @Override public String toString() {
+        return "FocusNodeValidationFinishedEvent{" +
+                        "focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationStartedEvent.java
similarity index 50%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationStartedEvent.java
index 3ced5f8efa..081945bb4b 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodeValidationStartedEvent.java
@@ -16,23 +16,25 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+/**
+ * Event emitted when a focus node has been validated completely with regard to a shape.
+ */
+public class FocusNodeValidationStartedEvent extends AbstractFocusNodeValidationEvent implements ValidationLifecycleEvent {
+    public FocusNodeValidationStartedEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode) {
+        super(vCxt, shape, focusNode);
+    }
 
-public class TC_SHACL { }
+    @Override public String toString() {
+        return "FocusNodeValidationStartedEvent{" +
+                        "focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodesDeterminedEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodesDeterminedEvent.java
new file mode 100644
index 0000000000..96a7944889
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/FocusNodesDeterminedEvent.java
@@ -0,0 +1,66 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
+
+import java.util.Collection;
+
+/**
+ * Event emitted when the focus node of a shape has been determined but before any constraints are validated.
+ */
+public class FocusNodesDeterminedEvent extends AbstractShapeValidationEvent implements ValidationLifecycleEvent {
+    protected final ImmutableLazyCollectionCopy<Node> focusNodes;
+
+    public FocusNodesDeterminedEvent(ValidationContext vCxt, Shape shape,
+                    Collection<Node> focusNodes) {
+        super(vCxt, shape);
+        this.focusNodes = new ImmutableLazyCollectionCopy<>(focusNodes);
+    }
+
+    public Collection<Node> getFocusNodes() {
+        return focusNodes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        FocusNodesDeterminedEvent that = (FocusNodesDeterminedEvent) o;
+        return getFocusNodes().equals(that.getFocusNodes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getFocusNodes().hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "FocusNodesDeterminedEvent{" +
+                        "shape=" + shape +
+                        ", focusNodes=" + focusNodes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ImmutableLazyCollectionCopy.java
similarity index 50%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ImmutableLazyCollectionCopy.java
index 3ced5f8efa..0b4c63e888 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ImmutableLazyCollectionCopy.java
@@ -16,23 +16,28 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+public class ImmutableLazyCollectionCopy<T> {
+    private final Collection<T> original;
+    private final AtomicReference<Collection<T>> copy = new AtomicReference<>();
 
-public class TC_SHACL { }
+    public ImmutableLazyCollectionCopy(Collection<T> original) {
+        this.original = original;
+    }
+
+    public Collection<T> get(){
+        return copy.updateAndGet(existingCopy -> existingCopy == null ? Collections.unmodifiableCollection(new ArrayList<>(original)) : existingCopy);
+    }
+
+    public String toString(){
+        return Optional.ofNullable(copy.get()).orElse(original).toString();
+    }
+
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ImmutableLazySetCopy.java
similarity index 55%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ImmutableLazySetCopy.java
index 3ced5f8efa..0a8ff91353 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ImmutableLazySetCopy.java
@@ -16,23 +16,25 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+public class ImmutableLazySetCopy<T> {
+    private final Set<T> original;
+    private final AtomicReference<Set<T>> copy = new AtomicReference<>();
 
-public class TC_SHACL { }
+    ImmutableLazySetCopy(Set<T> original){
+        this.original = original;
+    }
+
+    public Set<T> get(){
+        return copy.updateAndGet(existingCopy ->  existingCopy == null ? Set.copyOf(original) : existingCopy );
+    }
+
+    public String toString(){
+        return Optional.ofNullable(copy.get()).orElse(original).toString();
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationEvent.java
index 3ced5f8efa..895862bbc0 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationEvent.java
@@ -16,23 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+public interface ShapeValidationEvent extends ValidationEvent {
+    Shape getShape();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationFinishedEvent.java
similarity index 56%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationFinishedEvent.java
index 3ced5f8efa..ac1d3082d9 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationFinishedEvent.java
@@ -16,23 +16,22 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+/**
+ * Event emitted when the validation of a shape is finished.
+ */
+public class ShapeValidationFinishedEvent extends AbstractShapeValidationEvent implements ValidationLifecycleEvent {
+    public ShapeValidationFinishedEvent(ValidationContext vCxt, Shape shape) {
+        super(vCxt, shape);
+    }
 
-public class TC_SHACL { }
+    @Override public String toString() {
+        return "ShapeValidationFinishedEvent{" +
+                        "shape=" + shape +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationStartedEvent.java
similarity index 57%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationStartedEvent.java
index 3ced5f8efa..acdac61391 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ShapeValidationStartedEvent.java
@@ -16,23 +16,22 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+/**
+ * Event emitted when the validation of a shape has begun.
+ */
+public class ShapeValidationStartedEvent extends AbstractShapeValidationEvent implements ValidationLifecycleEvent {
+    public ShapeValidationStartedEvent(ValidationContext vCxt, Shape shape) {
+        super(vCxt, shape);
+    }
 
-public class TC_SHACL { }
+    @Override public String toString() {
+        return "ShapeValidationStartedEvent{" +
+                        "shape=" + shape +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/SingleCompareNodeEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/SingleCompareNodeEvent.java
index 3ced5f8efa..7f6ec07d0c 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/SingleCompareNodeEvent.java
@@ -16,23 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.graph.Node;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+public interface SingleCompareNodeEvent extends ConstraintEvaluationEvent {
+    Node getCompareNode();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapeValidationEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapeValidationEvent.java
index 3ced5f8efa..079d4b87a3 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapeValidationEvent.java
@@ -16,23 +16,12 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+import java.util.Collection;
 
-public class TC_SHACL { }
+public interface TargetShapeValidationEvent {
+    Collection<Shape> getTargetShapes();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapesValidationFinishedEvent.java
similarity index 51%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapesValidationFinishedEvent.java
index 3ced5f8efa..539f9f3762 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapesValidationFinishedEvent.java
@@ -16,23 +16,25 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+import java.util.Collection;
 
-public class TC_SHACL { }
+/**
+ * Event emitted when all target shapes (i.e., the shapes that specify a target) have been validated.
+ */
+public class TargetShapesValidationFinishedEvent extends AbstractTargetShapesValidationEvent implements ValidationLifecycleEvent {
+    public TargetShapesValidationFinishedEvent(ValidationContext vCxt,
+                    Collection<Shape> targetShapes) {
+        super(vCxt, targetShapes);
+    }
+
+    @Override public String toString() {
+        return "TargetShapesValidationFinishedEvent{" +
+                        "targetShapes=" + targetShapes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapesValidationStartedEvent.java
similarity index 51%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapesValidationStartedEvent.java
index 3ced5f8efa..d5dac8f5a7 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/TargetShapesValidationStartedEvent.java
@@ -16,23 +16,25 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
+import java.util.Collection;
 
-public class TC_SHACL { }
+/**
+ * Event emitted when the validation of the target shapes (i.e., the shapes that specify a target) starts.
+ */
+public class TargetShapesValidationStartedEvent extends AbstractTargetShapesValidationEvent implements ValidationLifecycleEvent {
+    public TargetShapesValidationStartedEvent(ValidationContext vCxt,
+                    Collection<Shape> targetShapes) {
+        super(vCxt, targetShapes);
+    }
+
+    @Override public String toString() {
+        return "TargetShapesValidationStartedEvent{" +
+                        "targetShapes=" + targetShapes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValidationEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValidationEvent.java
index 3ced5f8efa..9bc3c802d1 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValidationEvent.java
@@ -16,23 +16,13 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.shacl.engine.ValidationContext;
 
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+/**
+ * All events during SHACL validation implement this interface, providing access to the {@link org.apache.jena.ext.xerces.impl.dv.ValidationContext}.
+ */
+public interface ValidationEvent {
+    ValidationContext getValidationContext();
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValidationLifecycleEvent.java
similarity index 58%
copy from jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
copy to jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValidationLifecycleEvent.java
index 3ced5f8efa..a04babf684 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValidationLifecycleEvent.java
@@ -16,23 +16,11 @@
  * limitations under the License.
  */
 
-package org.apache.jena.shacl;
+package org.apache.jena.shacl.validation.event;
 
-import org.apache.jena.shacl.compact.TS_Compact;
-import org.apache.jena.shacl.tests.TestImports;
-import org.apache.jena.shacl.tests.TestValidationReport;
-import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
-import org.apache.jena.shacl.tests.std.TS_StdSHACL;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-
-@RunWith(Suite.class)
-@Suite.SuiteClasses( {
-    TestValidationReport.class
-    , TS_StdSHACL.class
-    , TS_JenaShacl.class
-    , TS_Compact.class
-    , TestImports.class
-} )
-
-public class TC_SHACL { }
+/**
+ * Tagging interface for all events relating to the 'lifecyle' of a SHACL
+ * validation (validation started/finished, focus nodes determined, etc.)
+ */
+public interface ValidationLifecycleEvent extends ValidationEvent {
+}
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValueNodesDeterminedForPropertyShapeEvent.java b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValueNodesDeterminedForPropertyShapeEvent.java
new file mode 100644
index 0000000000..5bf4c5c1e2
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/validation/event/ValueNodesDeterminedForPropertyShapeEvent.java
@@ -0,0 +1,79 @@
+/*
+ * 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.validation.event;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.sparql.path.Path;
+
+import java.util.Set;
+
+/**
+ * Event emitted when the value nodes of a property shape have been determined, but before any constraints are validated.
+ */
+public class ValueNodesDeterminedForPropertyShapeEvent extends AbstractFocusNodeValidationEvent
+                implements ValidationLifecycleEvent {
+    protected final Path path;
+    protected final ImmutableLazySetCopy<Node> pathNodes;
+
+    public ValueNodesDeterminedForPropertyShapeEvent(ValidationContext vCxt,
+                    Shape shape, Node focusNode, Path path,
+                    Set<Node> pathNodes) {
+        super(vCxt, shape, focusNode);
+        this.path = path;
+        this.pathNodes = new ImmutableLazySetCopy<>(pathNodes);
+    }
+
+    public Path getPath() {
+        return path;
+    }
+
+    public Set<Node> getPathNodes() {
+        return pathNodes.get();
+    }
+
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        ValueNodesDeterminedForPropertyShapeEvent that = (ValueNodesDeterminedForPropertyShapeEvent) o;
+        if (!getPath().equals(that.getPath()))
+            return false;
+        return getPathNodes().equals(that.getPathNodes());
+    }
+
+    @Override public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + getPath().hashCode();
+        result = 31 * result + getPathNodes().hashCode();
+        return result;
+    }
+
+    @Override public String toString() {
+        return "ValueNodesDeterminedForPropertyShapeEvent{" +
+                        "focusNode=" + focusNode +
+                        ", shape=" + shape +
+                        ", pathNodes=" + pathNodes +
+                        '}';
+    }
+}
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
index 3ced5f8efa..15f99f0b9a 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
@@ -21,6 +21,7 @@ package org.apache.jena.shacl;
 import org.apache.jena.shacl.compact.TS_Compact;
 import org.apache.jena.shacl.tests.TestImports;
 import org.apache.jena.shacl.tests.TestValidationReport;
+import org.apache.jena.shacl.tests.ValidationListenerTests;
 import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
 import org.apache.jena.shacl.tests.std.TS_StdSHACL;
 import org.junit.runner.RunWith;
@@ -33,6 +34,7 @@ import org.junit.runners.Suite;
     , TS_JenaShacl.class
     , TS_Compact.class
     , TestImports.class
+    , ValidationListenerTests.class
 } )
 
 public class TC_SHACL { }
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/tests/ValidationListenerTests.java b/jena-shacl/src/test/java/org/apache/jena/shacl/tests/ValidationListenerTests.java
new file mode 100644
index 0000000000..e32d0e2e1b
--- /dev/null
+++ b/jena-shacl/src/test/java/org/apache/jena/shacl/tests/ValidationListenerTests.java
@@ -0,0 +1,462 @@
+/*
+ * 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.tests;
+
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.impl.LiteralLabelFactory;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.shacl.Shapes;
+import org.apache.jena.shacl.engine.ValidationContext;
+import org.apache.jena.shacl.parser.Shape;
+import org.apache.jena.shacl.validation.VLib;
+import org.apache.jena.shacl.validation.ValidationListener;
+import org.apache.jena.shacl.validation.event.*;
+import org.apache.jena.vocabulary.XSD;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import static java.util.function.Predicate.not;
+import static org.apache.jena.shacl.validation.event.EventPredicates.*;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public class ValidationListenerTests {
+    private final String shapesGraphUri;
+    private final String dataGraphUri;
+    private final PredicateTreeNode predicateTree;
+
+    public ValidationListenerTests(String shapesGraphUri, String dataGraphUri,
+                    PredicateTreeNode predicateTree) {
+        this.shapesGraphUri = shapesGraphUri;
+        this.dataGraphUri = dataGraphUri;
+        this.predicateTree = predicateTree;
+    }
+
+    @SuppressWarnings("HttpUrlsUsage")
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+                        { "src/test/files/std/core/node/datatype-001.ttl",
+                                        "src/test/files/std/core/node/datatype-001.ttl",
+                                        EventTestBuilder
+                                                        .builder()
+                                                        .choice()
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().literalEquals(
+                                                                                                        LiteralLabelFactory.create("42",
+                                                                                                                        XSDDatatype.XSDinteger)))
+                                                                                        .and(shapeNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/datatype-001.test#TestShape")),
+                                                                        sb -> sb
+                                                                                        .nextVerify(
+                                                                                                        isOfType(ConstraintEvaluationForNodeShapeStartedEvent.class)
+                                                                                                                        .and(hasDatatypeConstraint()))
+                                                                                        .nextVerify(
+                                                                                                        isOfType(ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                                        .and(hasDatatypeConstraint())
+                                                                                                                        .and(isValid()))
+                                                                                        .nextVerify(
+                                                                                                        isOfType(ConstraintEvaluationForNodeShapeFinishedEvent.class)
+                                                                                                                        .and(hasDatatypeConstraint()))
+                                                                                        .nextVerify(
+                                                                                                        isOfType(FocusNodeValidationFinishedEvent.class)
+                                                                                                                        .and(not(hasDatatypeConstraint()))))
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().isBlank()),
+                                                                        sb -> sb
+                                                                                        .nextVerify(
+                                                                                                        isOfType(ConstraintEvaluationForNodeShapeStartedEvent.class)
+                                                                                                                        .and(hasDatatypeConstraint()))
+                                                                                        .nextVerify(
+                                                                                                        isOfType(ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                                        .and(not(isValid())))
+                                                                                        .nextVerify(
+                                                                                                        isOfType(ConstraintEvaluationForNodeShapeFinishedEvent.class)
+                                                                                                                        .and(shapeNode().uriEquals(
+                                                                                                                                        "http://datashapes.org/sh/tests/core/node/datatype-001.test#TestShape"))
+                                                                                        )
+                                                                                        .nextVerify(
+                                                                                                        isOfType(FocusNodeValidationFinishedEvent.class)
+                                                                                                                        .and(focusNode().isBlank()))
+                                                        )
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().literalEquals(
+                                                                                                        LiteralLabelFactory.create(
+                                                                                                                        "aldi",
+                                                                                                                        XSDDatatype.XSDinteger))),
+                                                                        sb -> sb
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeStartedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                        .and(hasDatatypeConstraint())
+                                                                                                        .and(not(isValid())))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeFinishedEvent.class).and(
+                                                                                                        focusNode().isLiteral()))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        FocusNodeValidationFinishedEvent.class)))
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().uriEquals(
+                                                                                                        XSD.integer.getURI())),
+                                                                        sb -> sb
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeStartedEvent.class)
+                                                                                                        .and(focusNode().uriEquals(
+                                                                                                                        XSD.integer.getURI())))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                        .and(focusNode().uriEquals(
+                                                                                                                        XSD.integer.getURI()))
+                                                                                                        .and(hasDatatypeConstraint())
+                                                                                                        .and(not(isValid())))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeFinishedEvent.class)
+                                                                                                        .and(focusNode().uriEquals(
+                                                                                                                        XSD.integer.getURI())))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        FocusNodeValidationFinishedEvent.class)
+                                                                                                        .and(focusNode().uriEquals(
+                                                                                                                        XSD.integer.getURI()))))
+                                                        .build()
+                        },
+                        {
+                                        "src/test/files/std/core/node/class-001.ttl",
+                                        "src/test/files/std/core/node/class-001.ttl",
+                                        EventTestBuilder.builder()
+                                                        .choice()
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#Someone"))
+                                                                                        .and(shapeNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#TestShape")),
+                                                                        sb -> sb
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeStartedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                        .and(isValid()))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeFinishedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        FocusNodeValidationFinishedEvent.class)))
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#John"))
+                                                                                        .and(shapeNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#TestShape")),
+                                                                        sb -> sb
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeStartedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                        .and(isValid()))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeFinishedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        FocusNodeValidationFinishedEvent.class)))
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#Typeless"))
+                                                                                        .and(shapeNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#TestShape")),
+                                                                        sb -> sb
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeStartedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                        .and(not(isValid())))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeFinishedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        FocusNodeValidationFinishedEvent.class)))
+                                                        .when(isOfType(FocusNodeValidationStartedEvent.class)
+                                                                                        .and(focusNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#Quokki"))
+                                                                                        .and(shapeNode().uriEquals(
+                                                                                                        "http://datashapes.org/sh/tests/core/node/class-001.test#TestShape")),
+                                                                        sb -> sb
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeStartedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluatedOnFocusNodeEvent.class)
+                                                                                                        .and(not(isValid())))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        ConstraintEvaluationForNodeShapeFinishedEvent.class))
+                                                                                        .nextVerify(isOfType(
+                                                                                                        FocusNodeValidationFinishedEvent.class)))
+                                                        .build()
+                        }
+        });
+    }
+
+    @Test
+    public void testOnlyExpectedEventsEmitted() {
+        Graph shapesGraph = RDFDataMgr.loadGraph(shapesGraphUri);
+        Graph dataGraph = RDFDataMgr.loadGraph(dataGraphUri);
+        RecordingValidationListener listener = new RecordingValidationListener();
+        Shapes shapes = Shapes.parse(shapesGraph);
+        ValidationContext vCtx = ValidationContext.create(shapes, dataGraph, listener);
+        for (Shape shape : shapes.getTargetShapes()) {
+            Collection<Node> focusNodes = VLib.focusNodes(dataGraph, shape);
+            for (Node focusNode : focusNodes) {
+                VLib.validateShape(vCtx, dataGraph, shape, focusNode);
+            }
+        }
+        List<ValidationEvent> actualEvents = listener.getEvents();
+        boolean allTestsRun = false;
+        PredicateTreeNode currentNode = predicateTree;
+        List<ValidationEvent> acceptedEvents = new ArrayList<>();
+        for (ValidationEvent e : actualEvents) {
+            if (allTestsRun) {
+                provideInfoForFailure(acceptedEvents);
+                fail("Spurious event: " + e);
+            }
+            TestResult testResult = currentNode.performTest(e);
+            if (testResult.isPassed()) {
+                acceptedEvents.add(e);
+            } else {
+                provideInfoForFailure(acceptedEvents);
+                fail("Event failed test: " + e);
+            }
+            currentNode = testResult.getNextNode();
+            if (currentNode == null) {
+                allTestsRun = true;
+            }
+        }
+        while (currentNode != null && currentNode != predicateTree) {
+            currentNode = currentNode.performTest(null).getNextNode();
+        }
+    }
+
+    private void provideInfoForFailure(List<ValidationEvent> acceptedEvents) {
+        System.err.println("Test failed!");
+        if (acceptedEvents.size() > 0) {
+            System.err.println("The following ValidationEvents were accepted before the test failed:");
+            for (ValidationEvent acceptedEvent : acceptedEvents) {
+                System.err.println("    event passed test: " + acceptedEvent);
+            }
+        } else {
+            System.err.println("No ValidationEvents were accepted before the test failed.");
+        }
+    }
+
+    private static class RecordingValidationListener implements ValidationListener {
+        private final List<ValidationEvent> events = new ArrayList<>();
+
+        @Override public void onValidationEvent(ValidationEvent e) {
+            if (events.contains(e)) {
+                fail(String.format("Duplicate event of type %s emitted by SHACL validation",
+                                e.getClass().getSimpleName()));
+            }
+            events.add(e);
+        }
+
+        public List<ValidationEvent> getEvents() {
+            return events;
+        }
+    }
+
+    private static class TestResult {
+        private final PredicateTreeNode nextNode;
+        private final boolean passed;
+        private final String message;
+
+        public TestResult(PredicateTreeNode nextNode, boolean passed, String message) {
+            this.nextNode = nextNode;
+            this.passed = passed;
+            this.message = message;
+        }
+
+        public PredicateTreeNode getNextNode() {
+            return nextNode;
+        }
+
+        public boolean isPassed() {
+            return passed;
+        }
+
+        @SuppressWarnings("unused")
+        public String getMessage() {
+            return message;
+        }
+    }
+
+    private static abstract class PredicateTreeNode {
+        private final PredicateTreeNode parent;
+
+        public PredicateTreeNode getParent() {
+            return parent;
+        }
+
+        public abstract TestResult performTest(ValidationEvent event);
+
+        private PredicateTreeNode(PredicateTreeNode parent) {
+            this.parent = parent;
+        }
+
+        protected TestResult deferTestToParent(ValidationEvent event) {
+            if (getParent() != null) {
+                return getParent().performTest(event);
+            } else if (event == null) {
+                return new TestResult(null, true, "Bubbling back up the test tree, not processing an event");
+            }
+            return new TestResult(null, false, "Spurious event");
+        }
+    }
+
+    private static class SequenceNode extends PredicateTreeNode {
+        private final List<PredicateTreeNode> children = new ArrayList<>();
+        private Iterator<PredicateTreeNode> childrenIterator;
+
+        public SequenceNode(PredicateTreeNode parent) {
+            super(parent);
+        }
+
+        public void addNode(PredicateTreeNode node) {
+            children.add(node);
+        }
+
+        private Iterator<PredicateTreeNode> getChildrenIteratorLazily() {
+            if (childrenIterator == null) {
+                childrenIterator = children.iterator();
+            }
+            return childrenIterator;
+        }
+
+        @Override public TestResult performTest(ValidationEvent event) {
+            PredicateTreeNode child = getNextChild();
+            if (child == null) {
+                return deferTestToParent(event);
+            }
+            return child.performTest(event);
+        }
+
+        private PredicateTreeNode getNextChild() {
+            Iterator<PredicateTreeNode> it = getChildrenIteratorLazily();
+            if (it.hasNext()) {
+                return it.next();
+            }
+            return null;
+        }
+    }
+
+    private static class LeafNode extends PredicateTreeNode {
+        private final Predicate<ValidationEvent> predicate;
+
+        public LeafNode(PredicateTreeNode parent,
+                        Predicate<ValidationEvent> predicate) {
+            super(parent);
+            this.predicate = predicate;
+        }
+
+        @Override public TestResult performTest(ValidationEvent event) {
+            return new TestResult(getParent(), predicate.test(event), "Result of predicate evaluation");
+        }
+    }
+
+    private static class ChoiceNode extends PredicateTreeNode {
+        private final Map<Predicate<ValidationEvent>, PredicateTreeNode> alternatives = new HashMap<>();
+        private final Set<Predicate<ValidationEvent>> alreadySelected = new HashSet<>();
+
+        public ChoiceNode(PredicateTreeNode parent) {
+            super(parent);
+        }
+
+        public void addAlternative(Predicate<ValidationEvent> key, PredicateTreeNode tree) {
+            this.alternatives.put(key, tree);
+        }
+
+        @Override public TestResult performTest(ValidationEvent event) {
+            if (alternatives.keySet().size() == alreadySelected.size()) {
+                // all alternatives have been verified, continue with parent
+                return deferTestToParent(event);
+            }
+            for (Predicate<ValidationEvent> possibleAlternative : alternatives.keySet()) {
+                if (possibleAlternative.test(event)) {
+                    if (alreadySelected.contains(possibleAlternative)) {
+                        return new TestResult(null, false,
+                                        "At least two events satisfy condition of choice node, this one was encountered second: "
+                                                        + event);
+                    }
+                    alreadySelected.add(possibleAlternative);
+                    return new TestResult(alternatives.get(possibleAlternative), true,
+                                    "Positive evaluation of the choice condition consumes the event");
+                }
+            }
+            return new TestResult(null, false, "Unexpected event: " + event);
+        }
+    }
+
+    private static class EventTestBuilder {
+        private EventTestBuilder() {
+        }
+
+        public static SequenceBuilder builder() {
+            return new SequenceBuilder(null);
+        }
+    }
+
+    private static class ChoiceBuilder {
+        private final ChoiceNode product;
+
+        private ChoiceBuilder(PredicateTreeNode parent) {
+            this.product = new ChoiceNode(parent);
+        }
+
+        public ChoiceBuilder when(Predicate<ValidationEvent> condition, Consumer<SequenceBuilder> sequenceConfigurer) {
+            SequenceBuilder sequenceBuilder = new SequenceBuilder(product);
+            sequenceConfigurer.accept(sequenceBuilder);
+            this.product.addAlternative(condition, sequenceBuilder.build());
+            return this;
+        }
+
+        public ChoiceNode build() {
+            return this.product;
+        }
+    }
+
+    private static class SequenceBuilder {
+        private final SequenceNode product;
+
+        private SequenceBuilder(PredicateTreeNode parent) {
+            this.product = new SequenceNode(parent);
+        }
+
+        public SequenceBuilder nextVerify(Predicate<ValidationEvent> predicate) {
+            this.product.addNode(new LeafNode(product, predicate));
+            return this;
+        }
+
+        public ChoiceBuilder choice() {
+            return new ChoiceBuilder(product);
+        }
+
+        public SequenceNode build() {
+            return product;
+        }
+    }
+}