Skip to content

Commit 330a2d4

Browse files
committed
Correctly evaluate sh:node for property shapes
1 parent 233ce30 commit 330a2d4

File tree

5 files changed

+138
-57
lines changed

5 files changed

+138
-57
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
import org.slf4j.Logger;
3838
import org.slf4j.LoggerFactory;
3939

40-
import com.google.common.collect.Streams;
41-
4240
import org.eclipse.esmf.aspectmodel.shacl.constraint.Constraint;
4341
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinCountConstraint;
4442
import org.eclipse.esmf.aspectmodel.shacl.constraint.SparqlConstraint;
@@ -47,6 +45,8 @@
4745
import org.eclipse.esmf.aspectmodel.shacl.violation.EvaluationContext;
4846
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
4947

48+
import com.google.common.collect.Streams;
49+
5050
/**
5151
* Implementation of a SHACL engine that allows validation on a per-element basis: {@link #validateElement(Resource)} can be used to retrieve validation
5252
* results only for this specific resource.
@@ -56,6 +56,7 @@ public class ShaclValidator {
5656
private final List<Shape.Node> shapes;
5757
private final Map<Resource, List<Shape.Node>> shapesWithClassTargets;
5858
private final Model shapesModel;
59+
private final PathNodeRetriever retriever = new PathNodeRetriever();
5960

6061
/**
6162
* Constructor to provide a custom RDF model containing SHACL shapes
@@ -166,7 +167,6 @@ public List<Violation> validateElements( final List<Resource> elements ) {
166167

167168
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node shape, final Model resolvedModel ) {
168169
final List<Violation> violations = new ArrayList<>();
169-
final PathNodeRetriever retriever = new PathNodeRetriever();
170170
for ( final Shape.Property property : shape.properties() ) {
171171
for ( final Constraint constraint : property.attributes().constraints() ) {
172172
final List<Statement> reachableNodes = property.path().accept( element, retriever );
@@ -257,4 +257,8 @@ public List<Shape.Node> getShapes() {
257257
public Model getShapesModel() {
258258
return shapesModel;
259259
}
260+
261+
public PathNodeRetriever getPathNodeRetriever() {
262+
return retriever;
263+
}
260264
}

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

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@
3333
import org.apache.jena.rdf.model.StmtIterator;
3434
import org.apache.jena.util.PrintUtil;
3535
import org.apache.jena.vocabulary.RDF;
36-
37-
import com.google.common.collect.ImmutableMap;
38-
import com.google.common.collect.Streams;
39-
4036
import org.eclipse.esmf.aspectmodel.shacl.constraint.AllowedLanguagesConstraint;
4137
import org.eclipse.esmf.aspectmodel.shacl.constraint.AllowedValuesConstraint;
4238
import org.eclipse.esmf.aspectmodel.shacl.constraint.AndConstraint;
@@ -75,68 +71,81 @@
7571
import org.eclipse.esmf.aspectmodel.shacl.path.ZeroOrMorePath;
7672
import org.eclipse.esmf.aspectmodel.shacl.path.ZeroOrOnePath;
7773

74+
import com.google.common.collect.ImmutableMap;
75+
import com.google.common.collect.Streams;
76+
7877
/**
7978
* Takes an RDF model describing one or more SHACL shapes as input and parses them into {@link Shape}s
8079
*/
8180
public class ShapeLoader implements Function<Model, List<Shape.Node>> {
8281
private static final SHACL SHACL = new SHACL();
8382

83+
/**
84+
* When constraints are instantiated for a shape, the context is passed as input
85+
* @param path If the parent shape of the constraint is a property shape, the path determines what the property shape refers to
86+
*/
87+
private record ShapeContext(Statement statement, Optional<Path> path) {
88+
}
89+
8490
/**
8591
* Encodes the logic of how to build an instance of {@link Constraint} from given RDF predicates on the value node
8692
*/
87-
private final Map<Property, Function<Statement, Constraint>> constraintLoaders = ImmutableMap.<Property, Function<Statement, Constraint>> builder()
88-
.put( SHACL.class_(), statement -> new ClassConstraint( statement.getResource() ) )
89-
.put( SHACL.datatype(), statement -> new DatatypeConstraint( statement.getResource().getURI() ) )
90-
.put( SHACL.nodeKind(), statement -> new NodeKindConstraint( Shape.NodeKind.valueOf( statement.getResource().getLocalName() ) ) )
91-
.put( SHACL.minCount(), statement -> new MinCountConstraint( statement.getLiteral().getInt() ) )
92-
.put( SHACL.maxCount(), statement -> new MaxCountConstraint( statement.getLiteral().getInt() ) )
93-
.put( SHACL.minExclusive(), statement -> new MinExclusiveConstraint( statement.getLiteral() ) )
94-
.put( SHACL.minInclusive(), statement -> new MinInclusiveConstraint( statement.getLiteral() ) )
95-
.put( SHACL.maxExclusive(), statement -> new MaxExclusiveConstraint( statement.getLiteral() ) )
96-
.put( SHACL.maxInclusive(), statement -> new MaxInclusiveConstraint( statement.getLiteral() ) )
97-
.put( SHACL.minLength(), statement -> new MinLengthConstraint( statement.getLiteral().getInt() ) )
98-
.put( SHACL.maxLength(), statement -> new MaxLengthConstraint( statement.getLiteral().getInt() ) )
99-
.put( SHACL.pattern(), statement -> {
100-
String flagsString = Optional.ofNullable( statement.getSubject().getProperty( SHACL.flags() ) ).map( Statement::getString ).orElse( "" );
101-
return new PatternConstraint( buildPattern( statement.getLiteral().getString(), flagsString ) );
93+
private final Map<Property, Function<ShapeContext, Constraint>> constraintLoaders = ImmutableMap.<Property, Function<ShapeContext, Constraint>> builder()
94+
.put( SHACL.class_(), context -> new ClassConstraint( context.statement().getResource() ) )
95+
.put( SHACL.datatype(), context -> new DatatypeConstraint( context.statement().getResource().getURI() ) )
96+
.put( SHACL.nodeKind(), context -> new NodeKindConstraint( Shape.NodeKind.valueOf( context.statement().getResource().getLocalName() ) ) )
97+
.put( SHACL.minCount(), context -> new MinCountConstraint( context.statement().getLiteral().getInt() ) )
98+
.put( SHACL.maxCount(), context -> new MaxCountConstraint( context.statement().getLiteral().getInt() ) )
99+
.put( SHACL.minExclusive(), context -> new MinExclusiveConstraint( context.statement().getLiteral() ) )
100+
.put( SHACL.minInclusive(), context -> new MinInclusiveConstraint( context.statement().getLiteral() ) )
101+
.put( SHACL.maxExclusive(), context -> new MaxExclusiveConstraint( context.statement().getLiteral() ) )
102+
.put( SHACL.maxInclusive(), context -> new MaxInclusiveConstraint( context.statement().getLiteral() ) )
103+
.put( SHACL.minLength(), context -> new MinLengthConstraint( context.statement().getLiteral().getInt() ) )
104+
.put( SHACL.maxLength(), context -> new MaxLengthConstraint( context.statement().getLiteral().getInt() ) )
105+
.put( SHACL.pattern(), context -> {
106+
String flagsString = Optional.ofNullable( context.statement().getSubject().getProperty( SHACL.flags() ) ).map( Statement::getString ).orElse( "" );
107+
return new PatternConstraint( buildPattern( context.statement().getLiteral().getString(), flagsString ) );
102108
} )
103-
.put( SHACL.languageIn(), statement ->
104-
new AllowedLanguagesConstraint( statement.getResource().as( RDFList.class ).mapWith( rdfNode -> rdfNode.asLiteral().getString() ).toList() ) )
105-
.put( SHACL.uniqueLang(), statement -> new UniqueLangConstraint() )
106-
.put( SHACL.equals(), statement -> new EqualsConstraint( statement.getModel().createProperty( statement.getResource().getURI() ) ) )
107-
.put( SHACL.disjoint(), statement -> new DisjointConstraint( statement.getModel().createProperty( statement.getResource().getURI() ) ) )
108-
.put( SHACL.lessThan(), statement -> new LessThanConstraint( statement.getModel().createProperty( statement.getResource().getURI() ) ) )
109+
.put( SHACL.languageIn(), context ->
110+
new AllowedLanguagesConstraint(
111+
context.statement().getResource().as( RDFList.class ).mapWith( rdfNode -> rdfNode.asLiteral().getString() ).toList() ) )
112+
.put( SHACL.uniqueLang(), context -> new UniqueLangConstraint() )
113+
.put( SHACL.equals(), context -> new EqualsConstraint( context.statement().getModel().createProperty( context.statement().getResource().getURI() ) ) )
114+
.put( SHACL.disjoint(),
115+
context -> new DisjointConstraint( context.statement().getModel().createProperty( context.statement().getResource().getURI() ) ) )
116+
.put( SHACL.lessThan(),
117+
context -> new LessThanConstraint( context.statement().getModel().createProperty( context.statement().getResource().getURI() ) ) )
109118
.put( SHACL.lessThanOrEquals(),
110-
statement -> new LessThanOrEqualsConstraint( statement.getModel().createProperty( statement.getResource().getURI() ) ) )
111-
.put( SHACL.not(), statement -> new NotConstraint( constraints( statement.getObject().asResource() ).get( 0 ) ) )
112-
.put( SHACL.and(), statement -> new AndConstraint( nestedConstraintList( statement ) ) )
113-
.put( SHACL.or(), statement -> new OrConstraint( nestedConstraintList( statement ) ) )
114-
.put( SHACL.xone(), statement -> new XoneConstraint( nestedConstraintList( statement ) ) )
115-
.put( SHACL.node(), statement -> new NodeConstraint( nodeShape( statement.getObject().asResource() ) ) )
116-
.put( SHACL.in(), statement -> new AllowedValuesConstraint( statement.getResource().as( RDFList.class ).asJavaList() ) )
117-
.put( SHACL.closed(), statement -> {
118-
boolean closed = statement.getBoolean();
119+
context -> new LessThanOrEqualsConstraint( context.statement().getModel().createProperty( context.statement().getResource().getURI() ) ) )
120+
.put( SHACL.not(), context -> new NotConstraint( constraints( context.statement().getObject().asResource(), context.path() ).get( 0 ) ) )
121+
.put( SHACL.and(), context -> new AndConstraint( nestedConstraintList( context.statement(), context.path() ) ) )
122+
.put( SHACL.or(), context -> new OrConstraint( nestedConstraintList( context.statement(), context.path() ) ) )
123+
.put( SHACL.xone(), context -> new XoneConstraint( nestedConstraintList( context.statement(), context.path() ) ) )
124+
.put( SHACL.node(), context -> new NodeConstraint( context.path(), nodeShape( context.statement().getObject().asResource() ) ) )
125+
.put( SHACL.in(), context -> new AllowedValuesConstraint( context.statement().getResource().as( RDFList.class ).asJavaList() ) )
126+
.put( SHACL.closed(), context -> {
127+
boolean closed = context.statement().getBoolean();
119128
if ( !closed ) {
120129
throw new RuntimeException();
121130
}
122-
Set<Property> ignoredProperties = statement.getSubject().getProperty( SHACL.ignoredProperties() ).getResource()
131+
Set<Property> ignoredProperties = context.statement().getSubject().getProperty( SHACL.ignoredProperties() ).getResource()
123132
.as( RDFList.class )
124133
.asJavaList()
125134
.stream()
126135
.map( RDFNode::asResource )
127136
.map( Resource::getURI )
128-
.map( uri -> statement.getModel().createProperty( uri ) )
137+
.map( uri -> context.statement().getModel().createProperty( uri ) )
129138
.collect( Collectors.toSet() );
130139
return new ClosedConstraint( ignoredProperties );
131140
} )
132-
.put( SHACL.hasValue(), statement -> new HasValueConstraint( statement.getObject() ) )
133-
.put( SHACL.sparql(), statement -> {
134-
final Resource constraintNode = statement.getResource();
141+
.put( SHACL.hasValue(), context -> new HasValueConstraint( context.statement().getObject() ) )
142+
.put( SHACL.sparql(), context -> {
143+
final Resource constraintNode = context.statement().getResource();
135144
final String message = Optional.ofNullable( constraintNode.getProperty( SHACL.message() ) ).map( Statement::getString ).orElse( "" );
136145
return new SparqlConstraint( message, sparqlQuery( constraintNode ) );
137146
} )
138-
.put( SHACL.js(), statement -> {
139-
Resource constraintNode = statement.getResource();
147+
.put( SHACL.js(), context -> {
148+
Resource constraintNode = context.statement().getResource();
140149
JsLibrary library = jsLibrary( constraintNode.getProperty( SHACL.jsLibrary() ).getResource() );
141150
String functionName = constraintNode.getProperty( SHACL.jsFunctionName() ).getString();
142151
final String message = Optional.ofNullable( constraintNode.getProperty( SHACL.message() ) ).map( Statement::getString ).orElse( "" );
@@ -146,13 +155,13 @@ public class ShapeLoader implements Function<Model, List<Shape.Node>> {
146155

147156
private final Map<Resource, JsLibrary> jsLibraries = new HashMap<>();
148157

149-
private List<Constraint> nestedConstraintList( final Statement statement ) {
158+
private List<Constraint> nestedConstraintList( final Statement statement, final Optional<Path> path ) {
150159
return statement.getObject().as( RDFList.class ).asJavaList().stream()
151160
.filter( RDFNode::isResource )
152161
.map( RDFNode::asResource )
153162
.flatMap( resource -> resource.isURIResource()
154163
? nodeShape( resource ).attributes().constraints().stream()
155-
: shapeAttributes( resource ).constraints().stream() )
164+
: shapeAttributes( resource, path ).constraints().stream() )
156165
.toList();
157166
}
158167

@@ -188,7 +197,7 @@ public List<Shape.Node> apply( final Model model ) {
188197
.toList();
189198
}
190199

191-
private Shape.Attributes shapeAttributes( final Resource shapeNode ) {
200+
private Shape.Attributes shapeAttributes( final Resource shapeNode, final Optional<Path> path ) {
192201
final Optional<String> uri = Optional.ofNullable( shapeNode.getURI() );
193202
final Optional<Resource> targetNode = Optional.ofNullable( shapeNode.getProperty( SHACL.targetNode() ) ).map( Statement::getResource );
194203
final Optional<Resource> targetClass = Optional.ofNullable( shapeNode.getProperty( SHACL.targetClass() ) ).map( Statement::getResource );
@@ -211,22 +220,22 @@ private Shape.Attributes shapeAttributes( final Resource shapeNode ) {
211220
final boolean deactivated = Optional.ofNullable( shapeNode.getProperty( SHACL.deactivated() ) ).map( Statement::getBoolean ).orElse( false );
212221
final Optional<String> message = Optional.ofNullable( shapeNode.getProperty( SHACL.message() ) ).map( Statement::getString );
213222
final Shape.Severity severity = severity( shapeNode );
214-
final List<Constraint> constraints = constraints( shapeNode );
223+
final List<Constraint> constraints = constraints( shapeNode, path );
215224
return new Shape.Attributes( uri, targetNode, targetClass, targetObjectsOf, targetSubjectsOf, targetSparql, name, description, order, group,
216225
defaultValue, deactivated, message, severity, constraints );
217226
}
218227

219228
private Shape.Node nodeShape( final Resource shapeNode ) {
220-
final Shape.Attributes attributes = shapeAttributes( shapeNode );
229+
final Shape.Attributes attributes = shapeAttributes( shapeNode, Optional.empty() );
221230
final List<Shape.Property> properties = Streams.stream( shapeNode.listProperties( SHACL.property() ) )
222231
.map( Statement::getResource )
223232
.map( this::propertyShape ).toList();
224233
return new Shape.Node( attributes, properties );
225234
}
226235

227236
private Shape.Property propertyShape( final Resource shapeNode ) {
228-
final Shape.Attributes attributes = shapeAttributes( shapeNode );
229237
final Path path = path( shapeNode.getProperty( SHACL.path() ).getResource() );
238+
final Shape.Attributes attributes = shapeAttributes( shapeNode, Optional.of( path ) );
230239
return new Shape.Property( attributes, path );
231240
}
232241

@@ -282,12 +291,12 @@ private boolean isRdfList( final Resource resource ) {
282291
|| resource.hasProperty( RDF.rest ) || resource.hasProperty( RDF.first );
283292
}
284293

285-
private List<Constraint> constraints( final Resource valueNode ) {
294+
private List<Constraint> constraints( final Resource valueNode, final Optional<Path> path ) {
286295
return constraintLoaders.entrySet().stream()
287296
.flatMap( entry -> {
288297
try {
289298
return Streams.stream( valueNode.listProperties( entry.getKey() ) )
290-
.map( statement -> entry.getValue().apply( statement ) );
299+
.map( statement -> entry.getValue().apply( new ShapeContext( statement, path ) ) );
291300
} catch ( final Exception exception ) {
292301
throw new RuntimeException( "Could not load SHACL shape: Invalid use of " + entry.getKey() + " on " + valueNode );
293302
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,34 @@
1414
package org.eclipse.esmf.aspectmodel.shacl.constraint;
1515

1616
import java.util.List;
17+
import java.util.Optional;
1718

1819
import org.apache.jena.rdf.model.RDFNode;
20+
import org.apache.jena.rdf.model.Statement;
1921
import org.eclipse.esmf.aspectmodel.shacl.Shape;
22+
import org.eclipse.esmf.aspectmodel.shacl.path.Path;
2023
import org.eclipse.esmf.aspectmodel.shacl.violation.EvaluationContext;
2124
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
2225

2326
/**
2427
* Implements <a href="https://www.w3.org/TR/shacl/#NodeConstraintComponent">sh:node</a>
25-
* @param shape the node shape this sh:node refers to
28+
* @param targetShape the node shape this sh:node refers to
2629
*/
27-
public record NodeConstraint(Shape.Node shape) implements Constraint {
30+
public record NodeConstraint(Optional<Path> path, Shape.Node targetShape) implements Constraint {
2831
@Override
2932
public List<Violation> apply( final RDFNode rdfNode, final EvaluationContext context ) {
30-
return context.validator().validateShapeForElement( context.element(), shape, context.resolvedModel() );
33+
// An empty path means that the node constraint is used inside a node shape, i.e., we just apply the target shape to the context element
34+
if ( path.isEmpty() ) {
35+
return context.validator().validateShapeForElement( context.element(), targetShape, context.resolvedModel() );
36+
}
37+
38+
// Having a path means that the node constraint is used inside a property shape, i.e., it applies to the element the
39+
// shape's path points to
40+
return path.get().accept( context.element(), context.validator().getPathNodeRetriever() ).stream()
41+
.filter( statement -> statement.getObject().isResource() )
42+
.map( Statement::getResource )
43+
.flatMap( element -> context.validator().validateShapeForElement( element, targetShape, context.resolvedModel() ).stream() )
44+
.toList();
3145
}
3246

3347
@Override

core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/validation/services/DetailedViolationFormatter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ public String visitMinLengthConstraint( final MinLengthConstraint constraint ) {
541541

542542
@Override
543543
public String visitNodeConstraint( final NodeConstraint constraint ) {
544-
return String.format( "shape-node: %s%n", constraint.shape().attributes().uri().map( violation::shortUri ).orElse( "(anonymous shape)" ) );
544+
return String.format( "shape-node: %s%n", constraint.targetShape().attributes().uri().map( violation::shortUri ).orElse( "(anonymous shape)" ) );
545545
}
546546

547547
@Override

0 commit comments

Comments
 (0)