22
22
import java .util .function .Function ;
23
23
import java .util .stream .Collectors ;
24
24
25
+ import org .eclipse .esmf .aspectmodel .resolver .services .VersionedModel ;
26
+ import org .eclipse .esmf .aspectmodel .shacl .constraint .Constraint ;
27
+ import org .eclipse .esmf .aspectmodel .shacl .constraint .MinCountConstraint ;
28
+ import org .eclipse .esmf .aspectmodel .shacl .constraint .SparqlConstraint ;
29
+ import org .eclipse .esmf .aspectmodel .shacl .path .PathNodeRetriever ;
30
+ import org .eclipse .esmf .aspectmodel .shacl .path .PredicatePath ;
31
+ import org .eclipse .esmf .aspectmodel .shacl .violation .EvaluationContext ;
32
+ import org .eclipse .esmf .aspectmodel .shacl .violation .Violation ;
33
+
34
+ import com .google .common .collect .Streams ;
25
35
import org .apache .jena .query .Query ;
26
36
import org .apache .jena .query .QueryExecution ;
27
37
import org .apache .jena .query .QueryExecutionFactory ;
33
43
import org .apache .jena .rdf .model .Resource ;
34
44
import org .apache .jena .rdf .model .Statement ;
35
45
import org .apache .jena .vocabulary .RDF ;
36
- import org .eclipse .esmf .aspectmodel .resolver .services .VersionedModel ;
37
- import org .eclipse .esmf .aspectmodel .shacl .constraint .Constraint ;
38
- import org .eclipse .esmf .aspectmodel .shacl .constraint .MinCountConstraint ;
39
- import org .eclipse .esmf .aspectmodel .shacl .constraint .SparqlConstraint ;
40
- import org .eclipse .esmf .aspectmodel .shacl .path .PathNodeRetriever ;
41
- import org .eclipse .esmf .aspectmodel .shacl .path .PredicatePath ;
42
- import org .eclipse .esmf .aspectmodel .shacl .violation .EvaluationContext ;
43
- import org .eclipse .esmf .aspectmodel .shacl .violation .Violation ;
44
-
45
- import com .google .common .collect .Streams ;
46
46
47
47
/**
48
- * Implementation of a SHACL engine that allows validation on a per-element basis: {@link #validateElement(Resource)} can be used to retrieve validation
49
- * results only for this specific resource.
48
+ * Implementation of a SHACL engine that allows validation on a per-element basis: {@link #validateElement(Resource)} can be used to
49
+ * retrieve validation results only for this specific resource.
50
50
*/
51
51
public class ShaclValidator {
52
52
private final List <Shape .Node > shapes ;
@@ -56,6 +56,7 @@ public class ShaclValidator {
56
56
57
57
/**
58
58
* Constructor to provide a custom RDF model containing SHACL shapes
59
+ *
59
60
* @param shapesModel the shapes model
60
61
*/
61
62
public ShaclValidator ( final Model shapesModel ) {
@@ -77,7 +78,9 @@ public ShaclValidator( final Model shapesModel ) {
77
78
* If you have more than one element to validate, prefer the method {@link #validateElements(List)} to calling this method in a loop
78
79
* for better performance.
79
80
* {@link Resource#getModel()} on the element must not return null, i.e., the resource may not be created using
80
- * {@link org.apache.jena.rdf.model.ResourceFactory#createProperty(String)}, but instead must be created via {@link Model#createResource(String)}.
81
+ * {@link org.apache.jena.rdf.model.ResourceFactory#createProperty(String)}, but instead must be created via
82
+ * {@link Model#createResource(String)}.
83
+ *
81
84
* @param element the element to be validated
82
85
* @return the list of {@link Violation}s if there are violations
83
86
*/
@@ -86,7 +89,8 @@ public List<Violation> validateElement( final Resource element ) {
86
89
return validateElement ( element , sparqlTargets , element .getModel () );
87
90
}
88
91
89
- private List <Violation > validateElement ( final Resource element , final Map <Resource , List <Shape .Node >> sparqlTargets , final Model resolvedModel ) {
92
+ private List <Violation > validateElement ( final Resource element , final Map <Resource , List <Shape .Node >> sparqlTargets ,
93
+ final Model resolvedModel ) {
90
94
final List <Violation > violations = new ArrayList <>();
91
95
for ( final Shape .Node shape : targetClassShapesThatApplyToElement ( element , resolvedModel ) ) {
92
96
violations .addAll ( validateShapeForElement ( element , shape , resolvedModel ) );
@@ -110,9 +114,11 @@ private List<Violation> validateElement( final Resource element, final Map<Resou
110
114
111
115
/**
112
116
* Validates a model using the SHACL shapes the validator was initialized with.
117
+ *
113
118
* @param model the model to be validated
114
119
* @return the list of {@link Violation}s if there are violations
115
120
*/
121
+ @ SuppressWarnings ( "UnstableApiUsage" ) // Usage of Streams.stream is deemed ok
116
122
public List <Violation > validateModel ( final VersionedModel model ) {
117
123
final Map <Resource , List <Shape .Node >> sparqlTargetsWithShapes = findSparqlTargets ( model .getRawModel () );
118
124
return Streams .stream ( model .getRawModel ().listStatements ( null , RDF .type , (RDFNode ) null ) )
@@ -158,41 +164,46 @@ private List<Resource> querySparqlTargets( final Model model, final Query query
158
164
}
159
165
160
166
public List <Violation > validateElements ( final List <Resource > elements ) {
161
- final Map <Resource , List <Shape .Node >> sparqlTargets = !elements .isEmpty () ? findSparqlTargets ( elements .get ( 0 ).getModel () ) : Map .of ();
167
+ final Map <Resource , List <Shape .Node >> sparqlTargets = !elements .isEmpty () ?
168
+ findSparqlTargets ( elements .get ( 0 ).getModel () ) : Map .of ();
162
169
return elements .stream ().flatMap ( element -> validateElement ( element , sparqlTargets , element .getModel () ).stream () ).toList ();
163
170
}
164
171
165
- public List <Violation > validateShapeForElement ( final Resource element , final Shape .Node shape , final Model resolvedModel ) {
172
+ public List <Violation > validateShapeForElement ( final Resource element , final Shape .Node nodeShape , final Model resolvedModel ) {
166
173
final List <Violation > violations = new ArrayList <>();
167
- for ( final Shape .Property property : shape .properties () ) {
168
- for ( final Constraint constraint : property .attributes ().constraints () ) {
169
- final List <Statement > reachableNodes = property .path ().accept ( element , retriever );
174
+ 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 );
170
177
// For all values that are present on the target node, check the applicable shapes and collect violations
171
178
for ( final Statement assertion : reachableNodes ) {
172
- final EvaluationContext context = new EvaluationContext ( element , shape , Optional .of ( assertion . getPredicate () ), reachableNodes , this ,
173
- resolvedModel );
179
+ final EvaluationContext context = new EvaluationContext ( element , nodeShape , Optional .of ( propertyShape ) ,
180
+ Optional . of ( assertion . getPredicate () ), List . of ( assertion ), this , resolvedModel );
174
181
violations .addAll ( constraint .apply ( assertion .getObject (), context ) );
175
182
}
176
183
177
184
// important detail: Sparql constraints must run independent of whether there are any matches via the sh:path property or not
178
185
// ( the check could be the verification whether the property exists )
179
186
if ( reachableNodes .isEmpty () && constraint instanceof SparqlConstraint ) {
180
- final EvaluationContext context = new EvaluationContext ( element , shape , Optional .empty (), List .of (), this , resolvedModel );
187
+ final EvaluationContext context = new EvaluationContext ( element , nodeShape , Optional .of ( propertyShape ), Optional .empty (),
188
+ List .of (), this , resolvedModel );
181
189
violations .addAll ( constraint .apply ( null , context ) );
182
190
}
183
191
184
- // MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints >= 1
185
- // exists, a violation must be emitted even though no value for the property exists
186
- if ( reachableNodes .isEmpty () && constraint instanceof MinCountConstraint && property .path () instanceof final PredicatePath predicatePath ) {
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 ) {
187
196
final Property rdfProperty = resolvedModel .createProperty ( predicatePath .predicate ().getURI () );
188
- final EvaluationContext context = new EvaluationContext ( element , shape , Optional .of ( rdfProperty ), List .of (), this , resolvedModel );
197
+ final EvaluationContext context = new EvaluationContext ( element , nodeShape , Optional .of ( propertyShape ),
198
+ Optional .of ( rdfProperty ), List .of (), this , resolvedModel );
189
199
violations .addAll ( constraint .apply ( null , context ) );
190
200
}
191
201
}
192
202
}
193
203
194
- final EvaluationContext context = new EvaluationContext ( element , shape , Optional .empty (), List .of (), this , resolvedModel );
195
- for ( final Constraint constraint : shape .attributes ().constraints () ) {
204
+ final EvaluationContext context = new EvaluationContext ( element , nodeShape , Optional .empty (), Optional .empty (), List .of (), this ,
205
+ resolvedModel );
206
+ for ( final Constraint constraint : nodeShape .attributes ().constraints () ) {
196
207
if ( !constraint .canBeUsedOnNodeShapes () ) {
197
208
continue ;
198
209
}
@@ -205,6 +216,7 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
205
216
/**
206
217
* Returns the shapes that apply to the element because the element has a type (or the type has a transitive supertype) that
207
218
* is given as sh:targetClass
219
+ *
208
220
* @param element a model element
209
221
* @return the stream of shapes
210
222
*/
@@ -217,6 +229,7 @@ private Set<Shape.Node> targetClassShapesThatApplyToElement( final Resource elem
217
229
218
230
/**
219
231
* Returns the shapes that apply to the element because the element uses a property which is given as sh:targetSubjectsOf
232
+ *
220
233
* @param element a model element
221
234
* @return the stream of shapes
222
235
*/
@@ -254,8 +267,4 @@ public List<Shape.Node> getShapes() {
254
267
public Model getShapesModel () {
255
268
return shapesModel ;
256
269
}
257
-
258
- public PathNodeRetriever getPathNodeRetriever () {
259
- return retriever ;
260
- }
261
270
}
0 commit comments