Skip to content

Commit 4551c72

Browse files
authored
Merge pull request #385 from bci-oss/373-fix-logical-shacl-components
Fix evaluation of logical SHACL components
2 parents 74f2412 + b26adad commit 4551c72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1001
-343
lines changed

core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/shacl/ShaclValidator.java

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.apache.jena.rdf.model.Property;
4242
import org.apache.jena.rdf.model.RDFNode;
4343
import org.apache.jena.rdf.model.Resource;
44+
import org.apache.jena.rdf.model.ResourceFactory;
4445
import org.apache.jena.rdf.model.Statement;
4546
import org.apache.jena.vocabulary.RDF;
4647

@@ -78,8 +79,7 @@ public ShaclValidator( final Model shapesModel ) {
7879
* If you have more than one element to validate, prefer the method {@link #validateElements(List)} to calling this method in a loop
7980
* for better performance.
8081
* {@link Resource#getModel()} on the element must not return null, i.e., the resource may not be created using
81-
* {@link org.apache.jena.rdf.model.ResourceFactory#createProperty(String)}, but instead must be created via
82-
* {@link Model#createResource(String)}.
82+
* {@link ResourceFactory#createProperty(String)}, but instead must be created via {@link Model#createResource(String)}.
8383
*
8484
* @param element the element to be validated
8585
* @return the list of {@link Violation}s if there are violations
@@ -170,45 +170,59 @@ public List<Violation> validateElements( final List<Resource> elements ) {
170170
}
171171

172172
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node nodeShape, final Model resolvedModel ) {
173+
return validateShapeForElement( element, nodeShape, resolvedModel, Optional.empty() );
174+
}
175+
176+
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node nodeShape, final Model resolvedModel,
177+
final Optional<EvaluationContext> parentContext ) {
173178
final List<Violation> violations = new ArrayList<>();
174179
for ( final Shape.Property propertyShape : nodeShape.properties() ) {
175-
for ( final Constraint constraint : propertyShape.attributes().constraints() ) {
176-
final List<Statement> reachableNodes = propertyShape.path().accept( element, retriever );
177-
// For all values that are present on the target node, check the applicable shapes and collect violations
178-
for ( final Statement assertion : reachableNodes ) {
179-
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
180-
Optional.of( assertion.getPredicate() ), List.of( assertion ), this, resolvedModel );
181-
violations.addAll( constraint.apply( assertion.getObject(), context ) );
182-
}
183-
184-
// important detail: Sparql constraints must run independent of whether there are any matches via the sh:path property or not
185-
// ( the check could be the verification whether the property exists )
186-
if ( reachableNodes.isEmpty() && constraint instanceof SparqlConstraint ) {
187-
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ), Optional.empty(),
188-
List.of(), this, resolvedModel );
189-
violations.addAll( constraint.apply( null, context ) );
190-
}
191-
192-
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints
193-
// >= 1 exists, a violation must be emitted even though no value for the property exists
194-
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint
195-
&& propertyShape.path() instanceof final PredicatePath predicatePath ) {
196-
final Property rdfProperty = resolvedModel.createProperty( predicatePath.predicate().getURI() );
197-
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
198-
Optional.of( rdfProperty ), List.of(), this, resolvedModel );
199-
violations.addAll( constraint.apply( null, context ) );
200-
}
201-
}
180+
violations.addAll( validateShapeForElement( element, nodeShape, propertyShape, resolvedModel, parentContext ) );
202181
}
203182

204-
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.empty(), Optional.empty(), List.of(), this,
205-
resolvedModel );
183+
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.empty(), Optional.empty(),
184+
parentContext, List.of(), this, resolvedModel );
206185
for ( final Constraint constraint : nodeShape.attributes().constraints() ) {
207186
if ( !constraint.canBeUsedOnNodeShapes() ) {
208187
continue;
209188
}
210189
violations.addAll( constraint.apply( element, context ) );
211190
}
191+
return violations;
192+
}
193+
194+
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node nodeShape, final Shape.Property propertyShape,
195+
final Model resolvedModel, final Optional<EvaluationContext> parentContext ) {
196+
final List<Violation> violations = new ArrayList<>();
197+
198+
for ( final Constraint constraint : propertyShape.attributes().constraints() ) {
199+
final List<Statement> reachableNodes = propertyShape.path().accept( element, retriever );
200+
// For all values that are present on the target node, check the applicable shapes and collect violations
201+
for ( final Statement assertion : reachableNodes ) {
202+
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
203+
Optional.of( assertion.getPredicate() ), parentContext, List.of( assertion ), this, resolvedModel );
204+
violations.addAll( constraint.apply( assertion.getObject(), context ) );
205+
}
206+
207+
// important detail: Sparql constraints must run independent of whether there are any matches via the sh:path property or not
208+
// ( the check could be the verification whether the property exists )
209+
if ( reachableNodes.isEmpty() && constraint instanceof SparqlConstraint ) {
210+
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ), Optional.empty(),
211+
parentContext, List.of(), this, resolvedModel );
212+
violations.addAll( constraint.apply( null, context ) );
213+
}
214+
215+
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints
216+
// >= 1 exists, a violation must be emitted even though no value for the property exists
217+
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint ) {
218+
final Optional<Property> property = propertyShape.path() instanceof PredicatePath ?
219+
Optional.of( resolvedModel.createProperty( ((PredicatePath) propertyShape.path()).predicate().getURI() ) ) :
220+
Optional.empty();
221+
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
222+
property, parentContext, List.of(), this, resolvedModel );
223+
violations.addAll( constraint.apply( null, context ) );
224+
}
225+
}
212226

213227
return violations;
214228
}
@@ -267,4 +281,8 @@ public List<Shape.Node> getShapes() {
267281
public Model getShapesModel() {
268282
return shapesModel;
269283
}
284+
285+
public PathNodeRetriever getRetriever() {
286+
return retriever;
287+
}
270288
}

core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/shacl/Shape.java

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,37 @@
1616
import java.util.List;
1717
import java.util.Optional;
1818

19+
import org.eclipse.esmf.aspectmodel.shacl.constraint.Constraint;
20+
import org.eclipse.esmf.aspectmodel.shacl.path.Path;
21+
1922
import org.apache.jena.query.Query;
2023
import org.apache.jena.rdf.model.RDFNode;
2124
import org.apache.jena.rdf.model.Resource;
2225

23-
import org.eclipse.esmf.aspectmodel.shacl.path.Path;
24-
import org.eclipse.esmf.aspectmodel.shacl.constraint.Constraint;
25-
2626
/**
2727
* Implements <a href="https://www.w3.org/TR/shacl/#node-shapes">sh:NodeShape</a>
2828
*/
2929
public interface Shape {
3030
Attributes attributes();
3131

32+
<T> T accept( Visitor<T> visitor );
33+
3234
/**
3335
* Implements the valid values for <a href="https://www.w3.org/TR/shacl/#NodeKindConstraintComponent">sh:nodeKind</a>
3436
*/
3537
enum NodeKind {
36-
BlankNode, IRI, Literal, BlankNodeOrIRI, BlankNodeOrLiteral, IRIOrLiteral;
38+
BlankNode( "an anonymous node" ),
39+
IRI( "a named element" ),
40+
Literal( "a value" ),
41+
BlankNodeOrIRI( "an anonymous node or a named element" ),
42+
BlankNodeOrLiteral( "an anonymous node or a value" ),
43+
IRIOrLiteral( "a named element or a value" );
44+
45+
private final String humanRepresentation;
46+
47+
NodeKind( final String humanRepresentation ) {
48+
this.humanRepresentation = humanRepresentation;
49+
}
3750

3851
public static NodeKind forNode( final RDFNode node ) {
3952
if ( node.isLiteral() ) {
@@ -47,6 +60,10 @@ public static NodeKind forNode( final RDFNode node ) {
4760
}
4861
throw new RuntimeException( "Invalid nodekind: " + node );
4962
}
63+
64+
public String humanReadable() {
65+
return humanRepresentation;
66+
}
5067
}
5168

5269
record Attributes(
@@ -76,6 +93,10 @@ record Node(
7693
Attributes attributes,
7794
List<Property> properties
7895
) implements Shape {
96+
@Override
97+
public <T> T accept( final Visitor<T> visitor ) {
98+
return visitor.visitNodeShape( this );
99+
}
79100
}
80101

81102
/**
@@ -85,6 +106,16 @@ record Property(
85106
Attributes attributes,
86107
Path path
87108
) implements Shape {
109+
@Override
110+
public <T> T accept( final Visitor<T> visitor ) {
111+
return visitor.visitPropertyShape( this );
112+
}
113+
}
114+
115+
interface Visitor<T> {
116+
T visitNodeShape( Shape.Node shapeNode );
117+
118+
T visitPropertyShape( Shape.Property propertyShape );
88119
}
89120
}
90121

0 commit comments

Comments
 (0)