Skip to content

Commit 44e5713

Browse files
committed
Fix evaluation of sh:and, sh:or and sh:xone
1 parent 1b7b1cb commit 44e5713

40 files changed

+753
-320
lines changed

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,18 @@ 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() );
173+
return validateShapeForElement( element, nodeShape, resolvedModel, Optional.empty() );
174174
}
175175

176176
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node nodeShape, final Model resolvedModel,
177-
final Optional<Resource> parentElement, final Optional<Property> parentProperty ) {
177+
final Optional<EvaluationContext> parentContext ) {
178178
final List<Violation> violations = new ArrayList<>();
179179
for ( final Shape.Property propertyShape : nodeShape.properties() ) {
180-
violations.addAll( validateShapeForElement( element, nodeShape, propertyShape, resolvedModel, parentElement, parentProperty ) );
180+
violations.addAll( validateShapeForElement( element, nodeShape, propertyShape, resolvedModel, parentContext ) );
181181
}
182182

183183
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.empty(), Optional.empty(),
184-
parentElement, parentProperty, List.of(), this, resolvedModel );
184+
parentContext, List.of(), this, resolvedModel );
185185
for ( final Constraint constraint : nodeShape.attributes().constraints() ) {
186186
if ( !constraint.canBeUsedOnNodeShapes() ) {
187187
continue;
@@ -192,24 +192,23 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
192192
}
193193

194194
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 ) {
195+
final Model resolvedModel, final Optional<EvaluationContext> parentContext ) {
196196
final List<Violation> violations = new ArrayList<>();
197197

198198
for ( final Constraint constraint : propertyShape.attributes().constraints() ) {
199199
final List<Statement> reachableNodes = propertyShape.path().accept( element, retriever );
200200
// For all values that are present on the target node, check the applicable shapes and collect violations
201201
for ( final Statement assertion : reachableNodes ) {
202202
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
203-
Optional.of( assertion.getPredicate() ),
204-
parentElement, parentProperty, List.of( assertion ), this, resolvedModel );
203+
Optional.of( assertion.getPredicate() ), parentContext, List.of( assertion ), this, resolvedModel );
205204
violations.addAll( constraint.apply( assertion.getObject(), context ) );
206205
}
207206

208207
// important detail: Sparql constraints must run independent of whether there are any matches via the sh:path property or not
209208
// ( the check could be the verification whether the property exists )
210209
if ( reachableNodes.isEmpty() && constraint instanceof SparqlConstraint ) {
211210
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ), Optional.empty(),
212-
parentElement, parentProperty, List.of(), this, resolvedModel );
211+
parentContext, List.of(), this, resolvedModel );
213212
violations.addAll( constraint.apply( null, context ) );
214213
}
215214

@@ -219,7 +218,7 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
219218
&& propertyShape.path() instanceof final PredicatePath predicatePath ) {
220219
final Property rdfProperty = resolvedModel.createProperty( predicatePath.predicate().getURI() );
221220
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
222-
Optional.of( rdfProperty ), parentElement, parentProperty, List.of(), this, resolvedModel );
221+
Optional.of( rdfProperty ), parentContext, List.of(), this, resolvedModel );
223222
violations.addAll( constraint.apply( null, context ) );
224223
}
225224
}

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

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ Constraint>> builder()
131131
context.statement().getModel().createProperty( context.statement().getResource().getURI() ) ) )
132132
.put( SHACL.not(), context ->
133133
new NotConstraint( constraints( context.statement().getObject().asResource(), context.path() ).get( 0 ) ) )
134-
.put( SHACL.and(), context -> new AndConstraint( nestedConstraintList( context.statement(), context.path() ) ) )
135-
.put( SHACL.or(), context -> new OrConstraint( nestedConstraintList( context.statement(), context.path() ) ) )
134+
.put( SHACL.and(), context -> new AndConstraint( nestedShapesList( context.statement() ) ) )
135+
.put( SHACL.or(), context -> new OrConstraint( nestedShapesList( context.statement() ) ) )
136136
.put( SHACL.xone(), context -> new XoneConstraint( nestedShapesList( context.statement() ) ) )
137137
.put( SHACL.node(), context -> {
138138
// Since sh:node can recursively refer to the same NodeShape is used in when shapes define recursive structures,
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH
3+
*
4+
* See the AUTHORS file(s) distributed with this work for additional
5+
* information regarding authorship.
6+
*
7+
* This Source Code Form is subject to the terms of the Mozilla Public
8+
* License, v. 2.0. If a copy of the MPL was not distributed with this
9+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
10+
*
11+
* SPDX-License-Identifier: MPL-2.0
12+
*/
13+
14+
package org.eclipse.esmf.aspectmodel.shacl;
15+
16+
import java.util.stream.Collectors;
17+
18+
import org.eclipse.esmf.aspectmodel.shacl.constraint.AllowedLanguagesConstraint;
19+
import org.eclipse.esmf.aspectmodel.shacl.constraint.AllowedValuesConstraint;
20+
import org.eclipse.esmf.aspectmodel.shacl.constraint.AndConstraint;
21+
import org.eclipse.esmf.aspectmodel.shacl.constraint.ClassConstraint;
22+
import org.eclipse.esmf.aspectmodel.shacl.constraint.Constraint;
23+
import org.eclipse.esmf.aspectmodel.shacl.constraint.DatatypeConstraint;
24+
import org.eclipse.esmf.aspectmodel.shacl.constraint.DisjointConstraint;
25+
import org.eclipse.esmf.aspectmodel.shacl.constraint.EqualsConstraint;
26+
import org.eclipse.esmf.aspectmodel.shacl.constraint.HasValueConstraint;
27+
import org.eclipse.esmf.aspectmodel.shacl.constraint.LessThanConstraint;
28+
import org.eclipse.esmf.aspectmodel.shacl.constraint.LessThanOrEqualsConstraint;
29+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MaxCountConstraint;
30+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MaxExclusiveConstraint;
31+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MaxInclusiveConstraint;
32+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MaxLengthConstraint;
33+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinCountConstraint;
34+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinExclusiveConstraint;
35+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinInclusiveConstraint;
36+
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinLengthConstraint;
37+
import org.eclipse.esmf.aspectmodel.shacl.constraint.NodeConstraint;
38+
import org.eclipse.esmf.aspectmodel.shacl.constraint.NodeKindConstraint;
39+
import org.eclipse.esmf.aspectmodel.shacl.constraint.NotConstraint;
40+
import org.eclipse.esmf.aspectmodel.shacl.constraint.OrConstraint;
41+
import org.eclipse.esmf.aspectmodel.shacl.constraint.PatternConstraint;
42+
import org.eclipse.esmf.aspectmodel.shacl.constraint.XoneConstraint;
43+
import org.eclipse.esmf.aspectmodel.shacl.violation.EvaluationContext;
44+
45+
import org.apache.jena.rdf.model.Property;
46+
47+
/**
48+
* Creates a short textual summary of a {@link Shape} or a {@link Constraint}.
49+
*/
50+
public class ShapeSummarizer implements Shape.Visitor<String>, Constraint.Visitor<String> {
51+
private final EvaluationContext context;
52+
53+
public ShapeSummarizer( final EvaluationContext context ) {
54+
this.context = context;
55+
}
56+
57+
@Override
58+
public String visitNodeShape( final Shape.Node nodeShape ) {
59+
return "%s: %s".formatted( nodeShape.attributes().uri().map( context::shortUri ).orElse( "(anonymous node shape)" ),
60+
nodeShape.attributes().constraints().stream().map( constraint ->
61+
constraint.accept( this ) ).collect( Collectors.joining( ", " ) ) );
62+
}
63+
64+
@Override
65+
public String visitPropertyShape( final Shape.Property propertyShape ) {
66+
return "%s with path %s: %s".formatted(
67+
propertyShape.attributes().uri().map( context::shortUri ).orElse( "(anonymous property shape)" ), propertyShape.path(),
68+
propertyShape.attributes().constraints().stream().map( constraint ->
69+
constraint.accept( this ) ).collect( Collectors.joining( ", " ) ) );
70+
}
71+
72+
/**
73+
* Default implementation for those constraints that either have no args (e.g. sh:closed) or for which the args should be
74+
* excluded since they could be too long (e.g. sh:SPARQLConstraint).
75+
*
76+
* @param constraint the constraint
77+
* @return a short string representation of the constraint
78+
*/
79+
@Override
80+
public String visit( final Constraint constraint ) {
81+
return constraint.name();
82+
}
83+
84+
@Override
85+
public String visitAllowedLanguagesConstraint( final AllowedLanguagesConstraint constraint ) {
86+
return "%s (allowed languages: %s)".formatted( constraint.name(), constraint.allowedLanguages() );
87+
}
88+
89+
@Override
90+
public String visitAllowedValuesConstraint( final AllowedValuesConstraint constraint ) {
91+
return "%s (allowed values: %s)".formatted( constraint.name(), constraint.allowedValues().stream().map( context::value ).toList() );
92+
}
93+
94+
@Override
95+
public String visitAndConstraint( final AndConstraint constraint ) {
96+
return "%s (with referenced shapes: %s)".formatted( constraint.name(),
97+
constraint.shapes().stream().map( shape -> shape.accept( this ) ) );
98+
}
99+
100+
@Override
101+
public String visitClassConstraint( final ClassConstraint constraint ) {
102+
return "%s (allowed class: %s)".formatted( constraint.name(), context.shortUri( constraint.allowedClass().getURI() ) );
103+
}
104+
105+
@Override
106+
public String visitDatatypeConstraint( final DatatypeConstraint constraint ) {
107+
return "%s (allowed type: %s)".formatted( constraint.name(), context.shortUri( constraint.allowedTypeUri() ) );
108+
}
109+
110+
@Override
111+
public String visitDisjointConstraint( final DisjointConstraint constraint ) {
112+
return "%s (disjoint with: %s which has value %s)".formatted( constraint.name(),
113+
context.shortUri( constraint.otherProperty().getURI() ), otherPropertyValue( constraint.otherProperty() ) );
114+
}
115+
116+
@Override
117+
public String visitEqualsConstraint( final EqualsConstraint constraint ) {
118+
return "%s (equal to: %s which has value %s)".formatted( constraint.name(),
119+
context.shortUri( constraint.otherProperty().getURI() ), otherPropertyValue( constraint.otherProperty() ) );
120+
}
121+
122+
@Override
123+
public String visitHasValueConstraint( final HasValueConstraint constraint ) {
124+
return "%s (allowed value: %s)".formatted( constraint.name(), context.value( constraint.allowedValue() ) );
125+
}
126+
127+
@Override
128+
public String visitLessThanConstraint( final LessThanConstraint constraint ) {
129+
return "%s (less than to: %s which has value %s)".formatted( constraint.name(),
130+
context.shortUri( constraint.otherProperty().getURI() ), otherPropertyValue( constraint.otherProperty() ) );
131+
}
132+
133+
@Override
134+
public String visitLessThanOrEqualsConstraint( final LessThanOrEqualsConstraint constraint ) {
135+
return "%s (less than or equal to: %s which has value %s)".formatted( constraint.name(),
136+
context.shortUri( constraint.otherProperty().getURI() ), otherPropertyValue( constraint.otherProperty() ) );
137+
}
138+
139+
@Override
140+
public String visitMaxCountConstraint( final MaxCountConstraint constraint ) {
141+
return "%s (max occurrences: %d)".formatted( constraint.name(), constraint.maxCount() );
142+
}
143+
144+
@Override
145+
public String visitMaxExclusiveConstraint( final MaxExclusiveConstraint constraint ) {
146+
return "%s (max value: %s)".formatted( constraint.name(), context.value( constraint.maxValue() ) );
147+
}
148+
149+
@Override
150+
public String visitMaxInclusiveConstraint( final MaxInclusiveConstraint constraint ) {
151+
return "%s (max value: %s)".formatted( constraint.name(), context.value( constraint.maxValue() ) );
152+
}
153+
154+
@Override
155+
public String visitMaxLengthConstraint( final MaxLengthConstraint constraint ) {
156+
return "%s (max length: %d)".formatted( constraint.name(), constraint.maxLength() );
157+
}
158+
159+
@Override
160+
public String visitMinCountConstraint( final MinCountConstraint constraint ) {
161+
return "%s (min occurrences: %d)".formatted( constraint.name(), constraint.minCount() );
162+
}
163+
164+
@Override
165+
public String visitMinExclusiveConstraint( final MinExclusiveConstraint constraint ) {
166+
return "%s (min value: %s)".formatted( constraint.name(), context.value( constraint.minValue() ) );
167+
}
168+
169+
@Override
170+
public String visitMinInclusiveConstraint( final MinInclusiveConstraint constraint ) {
171+
return "%s (min value: %s)".formatted( constraint.name(), context.value( constraint.minValue() ) );
172+
}
173+
174+
@Override
175+
public String visitMinLengthConstraint( final MinLengthConstraint constraint ) {
176+
return "%s (min length: %d)".formatted( constraint.name(), constraint.minLength() );
177+
}
178+
179+
@Override
180+
public String visitNodeConstraint( final NodeConstraint constraint ) {
181+
return "%s (referenced shape: %s)".formatted( constraint.name(), constraint.targetShape().get().accept( this ) );
182+
}
183+
184+
@Override
185+
public String visitNodeKindConstraint( final NodeKindConstraint constraint ) {
186+
return "%s (allowed node kind: %s)".formatted( constraint.name(), constraint.allowedNodeKind().humanReadable() );
187+
}
188+
189+
@Override
190+
public String visitNotConstraint( final NotConstraint constraint ) {
191+
return "%s (negated: %s)".formatted( constraint.name(), constraint.constraint().accept( this ) );
192+
}
193+
194+
@Override
195+
public String visitOrConstraint( final OrConstraint constraint ) {
196+
return "%s (with referenced shapes: %s)".formatted( constraint.name(),
197+
constraint.shapes().stream().map( shape -> shape.accept( this ) ) );
198+
}
199+
200+
@Override
201+
public String visitPatternConstraint( final PatternConstraint constraint ) {
202+
return "%s (pattern: %s)".formatted( constraint.name(), constraint.pattern() );
203+
}
204+
205+
@Override
206+
public String visitXoneConstraint( final XoneConstraint constraint ) {
207+
return "%s (with referenced shapes: %s)".formatted( constraint.name(),
208+
constraint.shapes().stream().map( shape -> shape.accept( this ) ) );
209+
}
210+
211+
protected String otherPropertyValue( final Property property ) {
212+
return context.value( context.element().getProperty( property ).getObject() );
213+
}
214+
}

0 commit comments

Comments
 (0)