Skip to content

Commit 9969fd9

Browse files
committed
Fix loading and evaluation of sh:xone constraints
1 parent 78de46f commit 9969fd9

File tree

4 files changed

+160
-75
lines changed

4 files changed

+160
-75
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(), Optional.empty() );
174+
}
175+
176+
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node nodeShape, final Model resolvedModel,
177+
final Optional<Resource> parentElement, final Optional<Property> parentProperty ) {
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, parentElement, parentProperty ) );
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+
parentElement, parentProperty, 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<Resource> parentElement, final Optional<Property> parentProperty ) {
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() ),
204+
parentElement, parentProperty, List.of( assertion ), this, resolvedModel );
205+
violations.addAll( constraint.apply( assertion.getObject(), context ) );
206+
}
207+
208+
// important detail: Sparql constraints must run independent of whether there are any matches via the sh:path property or not
209+
// ( the check could be the verification whether the property exists )
210+
if ( reachableNodes.isEmpty() && constraint instanceof SparqlConstraint ) {
211+
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ), Optional.empty(),
212+
parentElement, parentProperty, List.of(), this, resolvedModel );
213+
violations.addAll( constraint.apply( null, context ) );
214+
}
215+
216+
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints
217+
// >= 1 exists, a violation must be emitted even though no value for the property exists
218+
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint
219+
&& propertyShape.path() instanceof final PredicatePath predicatePath ) {
220+
final Property rdfProperty = resolvedModel.createProperty( predicatePath.predicate().getURI() );
221+
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
222+
Optional.of( rdfProperty ), parentElement, parentProperty, 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
}

0 commit comments

Comments
 (0)