Skip to content

Commit 305d6b0

Browse files
authored
Merge pull request #300 from bci-oss/bugfix/OMP-SDK-283-fix-shacl-validator
Include shapes with all target types in the validation.
2 parents 0683737 + 7fcbc91 commit 305d6b0

File tree

5 files changed

+451
-21
lines changed

5 files changed

+451
-21
lines changed

core/sds-aspect-model-validator/src/main/java/io/openmanufacturing/sds/aspectmodel/shacl/ShaclValidator.java

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,34 @@
1414
package io.openmanufacturing.sds.aspectmodel.shacl;
1515

1616
import java.util.ArrayList;
17+
import java.util.HashMap;
1718
import java.util.List;
1819
import java.util.Map;
1920
import java.util.Optional;
2021
import java.util.Set;
2122
import java.util.function.Function;
2223
import java.util.stream.Collectors;
2324

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;
2430
import org.apache.jena.rdf.model.Model;
2531
import org.apache.jena.rdf.model.Property;
32+
import org.apache.jena.rdf.model.RDFNode;
2633
import org.apache.jena.rdf.model.Resource;
2734
import org.apache.jena.rdf.model.Statement;
35+
import org.apache.jena.vocabulary.RDF;
2836
import org.slf4j.Logger;
2937
import org.slf4j.LoggerFactory;
3038

39+
import com.google.common.collect.Streams;
40+
41+
import io.openmanufacturing.sds.aspectmodel.resolver.services.VersionedModel;
3142
import io.openmanufacturing.sds.aspectmodel.shacl.constraint.Constraint;
3243
import io.openmanufacturing.sds.aspectmodel.shacl.constraint.MinCountConstraint;
44+
import io.openmanufacturing.sds.aspectmodel.shacl.constraint.SparqlConstraint;
3345
import io.openmanufacturing.sds.aspectmodel.shacl.path.PathNodeRetriever;
3446
import io.openmanufacturing.sds.aspectmodel.shacl.path.PredicatePath;
3547
import io.openmanufacturing.sds.aspectmodel.shacl.violation.EvaluationContext;
@@ -65,24 +77,93 @@ public ShaclValidator( final Model shapesModel ) {
6577

6678
/**
6779
* 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.
6882
* {@link Resource#getModel()} on the element must not return null, i.e., the resource may not be created using
6983
* {@link org.apache.jena.rdf.model.ResourceFactory#createProperty(String)}, but instead must be created via {@link Model#createResource(String)}.
7084
* @param element the element to be validated
7185
* @return the list of {@link Violation}s if there are violations
7286
*/
7387
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 ) {
7493
final List<Violation> violations = new ArrayList<>();
7594
for ( final Shape.Node shape : targetClassShapesThatApplyToElement( element ) ) {
7695
violations.addAll( validateShapeForElement( element, shape ) );
7796
}
7897
for ( final Shape.Node shape : targetSubjectShapesThatApplyToElement( element ) ) {
7998
violations.addAll( validateShapeForElement( element, shape ) );
8099
}
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+
}
81111
return violations;
82112
}
83113

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+
84164
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();
86167
}
87168

88169
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node shape ) {
@@ -98,6 +179,13 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
98179
violations.addAll( constraint.apply( assertion.getObject(), context ) );
99180
}
100181

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+
101189
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints >= 1
102190
// exists, a violation must be emitted even though no value for the property exists
103191
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint && property.path() instanceof PredicatePath predicatePath ) {
@@ -144,6 +232,26 @@ private Set<Shape.Node> targetSubjectShapesThatApplyToElement( final Resource el
144232
.collect( Collectors.toSet() );
145233
}
146234

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+
147255
public List<Shape.Node> getShapes() {
148256
return shapes;
149257
}

core/sds-aspect-model-validator/src/main/java/io/openmanufacturing/sds/aspectmodel/validation/services/AspectModelValidator.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@
3232
import org.apache.jena.rdf.model.Resource;
3333
import org.apache.jena.rdf.model.Statement;
3434
import org.apache.jena.riot.RiotException;
35-
import org.apache.jena.vocabulary.RDF;
3635
import org.slf4j.Logger;
3736
import org.slf4j.LoggerFactory;
3837
import org.topbraid.shacl.validation.ValidationEngineFactory;
3938
import org.topbraid.shacl.validation.ValidationUtil;
4039
import org.topbraid.shacl.vocabulary.SH;
4140

4241
import com.google.common.collect.ImmutableList;
43-
import com.google.common.collect.Streams;
4442

4543
import io.openmanufacturing.sds.aspectmetamodel.KnownVersion;
4644
import io.openmanufacturing.sds.aspectmodel.UnsupportedVersionException;
@@ -157,13 +155,7 @@ public List<Violation> validateModel( final Try<VersionedModel> versionedModel )
157155
}
158156
// Determine violations for all model elements
159157
final VersionedModel model = versionedModel.get();
160-
final List<Violation> result = Streams.stream( model.getRawModel().listStatements( null, RDF.type, (RDFNode) null ) )
161-
.map( Statement::getSubject )
162-
.filter( Resource::isURIResource )
163-
.map( Resource::getURI )
164-
.map( uri -> model.getModel().createResource( uri ) )
165-
.flatMap( element -> shaclValidator.validateElement( element ).stream() )
166-
.toList();
158+
final List<Violation> result = shaclValidator.validateModel( model );
167159

168160
if ( result.isEmpty() ) {
169161
// The SHACL validation succeeded, check for cycles in the model.

0 commit comments

Comments
 (0)