14
14
package io .openmanufacturing .sds .aspectmodel .shacl ;
15
15
16
16
import java .util .ArrayList ;
17
+ import java .util .HashMap ;
17
18
import java .util .List ;
18
19
import java .util .Map ;
19
20
import java .util .Optional ;
20
21
import java .util .Set ;
21
22
import java .util .function .Function ;
22
23
import java .util .stream .Collectors ;
23
24
25
+ import org .apache .jena .query .Query ;
26
+ import org .apache .jena .query .QueryExecution ;
27
+ import org .apache .jena .query .QueryExecutionFactory ;
28
+ import org .apache .jena .query .QuerySolution ;
29
+ import org .apache .jena .query .ResultSet ;
24
30
import org .apache .jena .rdf .model .Model ;
25
31
import org .apache .jena .rdf .model .Property ;
32
+ import org .apache .jena .rdf .model .RDFNode ;
26
33
import org .apache .jena .rdf .model .Resource ;
27
34
import org .apache .jena .rdf .model .Statement ;
35
+ import org .apache .jena .vocabulary .RDF ;
28
36
import org .slf4j .Logger ;
29
37
import org .slf4j .LoggerFactory ;
30
38
39
+ import com .google .common .collect .Streams ;
40
+
41
+ import io .openmanufacturing .sds .aspectmodel .resolver .services .VersionedModel ;
31
42
import io .openmanufacturing .sds .aspectmodel .shacl .constraint .Constraint ;
32
43
import io .openmanufacturing .sds .aspectmodel .shacl .constraint .MinCountConstraint ;
44
+ import io .openmanufacturing .sds .aspectmodel .shacl .constraint .SparqlConstraint ;
33
45
import io .openmanufacturing .sds .aspectmodel .shacl .path .PathNodeRetriever ;
34
46
import io .openmanufacturing .sds .aspectmodel .shacl .path .PredicatePath ;
35
47
import io .openmanufacturing .sds .aspectmodel .shacl .violation .EvaluationContext ;
@@ -65,24 +77,93 @@ public ShaclValidator( final Model shapesModel ) {
65
77
66
78
/**
67
79
* Validates a model element using the SHACL shapes the validator was initialized with.
80
+ * If you have more than one element to validate, prefer the method {@link this.validateElements( final List<Resource> )} to calling this method in a loop
81
+ * for better performance.
68
82
* {@link Resource#getModel()} on the element must not return null, i.e., the resource may not be created using
69
83
* {@link org.apache.jena.rdf.model.ResourceFactory#createProperty(String)}, but instead must be created via {@link Model#createResource(String)}.
70
84
* @param element the element to be validated
71
85
* @return the list of {@link Violation}s if there are violations
72
86
*/
73
87
public List <Violation > validateElement ( final Resource element ) {
88
+ final Map <Resource , List <Shape .Node >> sparqlTargets = findSparqlTargets ( element .getModel () );
89
+ return validateElement ( element , sparqlTargets );
90
+ }
91
+
92
+ private List <Violation > validateElement ( final Resource element , final Map <Resource , List <Shape .Node >> sparqlTargets ) {
74
93
final List <Violation > violations = new ArrayList <>();
75
94
for ( final Shape .Node shape : targetClassShapesThatApplyToElement ( element ) ) {
76
95
violations .addAll ( validateShapeForElement ( element , shape ) );
77
96
}
78
97
for ( final Shape .Node shape : targetSubjectShapesThatApplyToElement ( element ) ) {
79
98
violations .addAll ( validateShapeForElement ( element , shape ) );
80
99
}
100
+ for ( final Shape .Node shape : targetObjectShapesThatApplyToElement ( element ) ) {
101
+ violations .addAll ( validateShapeForElement ( element , shape ) );
102
+ }
103
+ for ( final Shape .Node shape : targetNodeShapesThatApplyToElement ( element ) ) {
104
+ violations .addAll ( validateShapeForElement ( element , shape ) );
105
+ }
106
+ if ( sparqlTargets .containsKey ( element ) ) {
107
+ for ( final Shape .Node shape : sparqlTargets .get ( element ) ) {
108
+ violations .addAll ( validateShapeForElement ( element , shape ) );
109
+ }
110
+ }
81
111
return violations ;
82
112
}
83
113
114
+ /**
115
+ * Validates a model using the SHACL shapes the validator was initialized with.
116
+ * @param model the model to be validated
117
+ * @return the list of {@link Violation}s if there are violations
118
+ */
119
+ public List <Violation > validateModel ( final VersionedModel model ) {
120
+ final Map <Resource , List <Shape .Node >> sparqlTargetsWithShapes = findSparqlTargets ( model .getModel () );
121
+ return Streams .stream ( model .getRawModel ().listStatements ( null , RDF .type , (RDFNode ) null ) )
122
+ .map ( Statement ::getSubject )
123
+ .filter ( Resource ::isURIResource )
124
+ .map ( Resource ::getURI )
125
+ .map ( uri -> model .getModel ().createResource ( uri ) )
126
+ .flatMap ( element -> validateElement ( element , sparqlTargetsWithShapes ).stream () )
127
+ .toList ();
128
+ }
129
+
130
+ private Map <Resource , List <Shape .Node >> findSparqlTargets ( final Model model ) {
131
+ final Map <Resource , List <Shape .Node >> resourceShapes = new HashMap <>();
132
+ for ( final Shape .Node shape : targetSparqlShapes () ) {
133
+ final List <Resource > shapeTargets = querySparqlTargets ( model , shape .attributes ().targetSparql ().get () );
134
+ for ( final Resource node : shapeTargets ) {
135
+ addResourceShape ( resourceShapes , node , shape );
136
+ }
137
+ }
138
+ return resourceShapes ;
139
+ }
140
+
141
+ // single resource can be sparql target to more than one shape
142
+ private void addResourceShape ( final Map <Resource , List <Shape .Node >> map , final Resource resource , final Shape .Node shape ) {
143
+ if ( map .containsKey ( resource ) ) {
144
+ map .get ( resource ).add ( shape );
145
+ } else {
146
+ final ArrayList <Shape .Node > shapes = new ArrayList <>();
147
+ shapes .add ( shape );
148
+ map .put ( resource , shapes );
149
+ }
150
+ }
151
+
152
+ private List <Resource > querySparqlTargets ( final Model model , final Query query ) {
153
+ final List <Resource > targets = new ArrayList <>();
154
+ try ( final QueryExecution queryExecution = QueryExecutionFactory .create ( query , model ) ) {
155
+ final ResultSet resultSet = queryExecution .execSelect ();
156
+ while ( resultSet .hasNext () ) {
157
+ final QuerySolution solution = resultSet .next ();
158
+ targets .add ( solution .getResource ( "this" ) );
159
+ }
160
+ }
161
+ return targets ;
162
+ }
163
+
84
164
public List <Violation > validateElements ( final List <Resource > elements ) {
85
- return elements .stream ().flatMap ( element -> validateElement ( element ).stream () ).toList ();
165
+ final Map <Resource , List <Shape .Node >> sparqlTargets = elements .size () > 0 ? findSparqlTargets ( elements .get ( 0 ).getModel () ) : Map .of ();
166
+ return elements .stream ().flatMap ( element -> validateElement ( element , sparqlTargets ).stream () ).toList ();
86
167
}
87
168
88
169
public List <Violation > validateShapeForElement ( final Resource element , final Shape .Node shape ) {
@@ -98,6 +179,13 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
98
179
violations .addAll ( constraint .apply ( assertion .getObject (), context ) );
99
180
}
100
181
182
+ // important detail: Sparql constraints must run independent of whether there are any matches via the sh:path property or not
183
+ // ( the check could be the verification whether the property exists )
184
+ if ( reachableNodes .isEmpty () && constraint instanceof SparqlConstraint ) {
185
+ final EvaluationContext context = new EvaluationContext ( element , shape , Optional .empty (), List .of (), this );
186
+ violations .addAll ( constraint .apply ( null , context ) );
187
+ }
188
+
101
189
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints >= 1
102
190
// exists, a violation must be emitted even though no value for the property exists
103
191
if ( reachableNodes .isEmpty () && constraint instanceof MinCountConstraint && property .path () instanceof PredicatePath predicatePath ) {
@@ -144,6 +232,26 @@ private Set<Shape.Node> targetSubjectShapesThatApplyToElement( final Resource el
144
232
.collect ( Collectors .toSet () );
145
233
}
146
234
235
+ private Set <Shape .Node > targetObjectShapesThatApplyToElement ( final Resource element ) {
236
+ return shapes .stream ()
237
+ .filter ( shape ->
238
+ shape .attributes ().targetObjectsOf ().map ( property -> element .getProperty ( property ) != null ).orElse ( false ) )
239
+ .collect ( Collectors .toSet () );
240
+ }
241
+
242
+ private Set <Shape .Node > targetNodeShapesThatApplyToElement ( final Resource element ) {
243
+ return shapes .stream ()
244
+ .filter ( shape ->
245
+ shape .attributes ().targetNode ().map ( element ::equals ).orElse ( false ) )
246
+ .collect ( Collectors .toSet () );
247
+ }
248
+
249
+ private Set <Shape .Node > targetSparqlShapes () {
250
+ return shapes .stream ()
251
+ .filter ( shape -> shape .attributes ().targetSparql ().isPresent () )
252
+ .collect ( Collectors .toSet () );
253
+ }
254
+
147
255
public List <Shape .Node > getShapes () {
148
256
return shapes ;
149
257
}
0 commit comments