Skip to content

Commit 0e32aac

Browse files
authored
Merge pull request #362 from bci-oss/361-fix-sh-node-validation
Fix evaluation of sh:node in sh:PropertyShapes
2 parents 233ce30 + b393bd7 commit 0e32aac

33 files changed

+363
-221
lines changed

core/esmf-aspect-model-validator/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
<artifactId>lombok</artifactId>
6767
<scope>provided</scope>
6868
</dependency>
69+
<dependency>
70+
<groupId>org.slf4j</groupId>
71+
<artifactId>slf4j-api</artifactId>
72+
</dependency>
6973

7074
<!-- Test dependencies -->
7175
<dependency>

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

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@
4343

4444
/**
4545
* Rust-like message formatter. Formatted messages look something like the following example:
46-
*
46+
* <br/><br/>
47+
* <pre>
4748
* ---> Error at line 11 column 20
4849
* |
4950
* 11 | :testProperty [ a :SomethingElse ]
5051
* | ^^^^^^^^^^^^^^ Property ':testProperty' on ':Foo' has type ':SomethingElse', but only ':TestClass2' is allowed.
5152
* |
53+
* </pre>
5254
*/
5355
public class RustLikeFormatter {
5456

@@ -228,9 +230,15 @@ private boolean formatNode( final RDFNode node ) {
228230
final SmartToken nodePosition = extractToken( node );
229231
if ( null == nodePosition ) {
230232
// ugly special case: Jena internally replaces the RDF keyword 'a' with 'rdf:type' without position information
231-
if ( node.asNode().equals( NodeConst.nodeRDFType ) ) {
233+
if ( NodeConst.nodeRDFType.equals( node.asNode() ) ) {
232234
spacedIfPossible( "a " );
233235
}
236+
if ( NodeConst.nodeTrue.equals( node.asNode() ) ) {
237+
spacedIfPossible( "true" );
238+
}
239+
if ( NodeConst.nodeFalse.equals( node.asNode() ) ) {
240+
spacedIfPossible( "false" );
241+
}
234242
return true;
235243
}
236244

@@ -254,15 +262,16 @@ private boolean isList( final Model model, final RDFNode node ) {
254262
return node.equals( RDF.nil ) || (node.isResource() && model.contains( node.asResource(), RDF.rest, (RDFNode) null ));
255263
}
256264

257-
private Statement findListHead( Statement listElement ) {
265+
private Statement findListHead( final Statement listElement ) {
266+
Statement listElementStatement = listElement;
258267
while ( true ) {
259-
final StmtIterator iter = listElement.getModel().listStatements( null, RDF.rest, listElement.getSubject() );
268+
final StmtIterator iter = listElementStatement.getModel().listStatements( null, RDF.rest, listElementStatement.getSubject() );
260269
if ( !iter.hasNext() ) {
261270
break;
262271
}
263-
listElement = iter.nextStatement();
272+
listElementStatement = iter.nextStatement();
264273
}
265-
return listElement;
274+
return listElementStatement;
266275
}
267276

268277
private boolean formatList( final RDFNode listNode ) {
@@ -341,17 +350,17 @@ private void formatText( final String reconstructedText ) {
341350
currentColumn += reconstructedText.length();
342351
}
343352

344-
private static SmartToken extractToken( final RDFNode node ) {
345-
final Node n = node.asNode();
346-
if ( n instanceof AnyNode an ) {
353+
private static SmartToken extractToken( final RDFNode rdfNode ) {
354+
final Node node = rdfNode.asNode();
355+
if ( node instanceof final AnyNode an ) {
347356
return an.getToken();
348-
} else if ( n instanceof BlankNode bn ) {
357+
} else if ( node instanceof final BlankNode bn ) {
349358
return bn.getToken();
350-
} else if ( n instanceof LiteralNode ln ) {
359+
} else if ( node instanceof final LiteralNode ln ) {
351360
return ln.getToken();
352-
} else if ( n instanceof UriNode un ) {
361+
} else if ( node instanceof final UriNode un ) {
353362
return un.getToken();
354-
} else if ( n instanceof VariableNode vn ) {
363+
} else if ( node instanceof final VariableNode vn ) {
355364
return vn.getToken();
356365
} else {
357366
return null;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* Vocabulary for the Shapes Constraint Language (SHACL)
2323
*/
2424
public class SHACL implements Namespace {
25-
public final String NS = "http://www.w3.org/ns/shacl#";
25+
public static final String NS = "http://www.w3.org/ns/shacl#";
2626

2727
@Override
2828
public String getUri() {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.io.Serial;
17+
18+
/**
19+
* This exception is thrown when the validation process fails
20+
*/
21+
public class ShaclValidationException extends RuntimeException {
22+
@Serial
23+
private static final long serialVersionUID = 7771592676699681415L;
24+
25+
public ShaclValidationException( final String message ) {
26+
super( message );
27+
}
28+
}

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@
3434
import org.apache.jena.rdf.model.Statement;
3535
import org.apache.jena.vocabulary.RDF;
3636
import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel;
37-
import org.slf4j.Logger;
38-
import org.slf4j.LoggerFactory;
39-
40-
import com.google.common.collect.Streams;
41-
4237
import org.eclipse.esmf.aspectmodel.shacl.constraint.Constraint;
4338
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinCountConstraint;
4439
import org.eclipse.esmf.aspectmodel.shacl.constraint.SparqlConstraint;
@@ -47,15 +42,17 @@
4742
import org.eclipse.esmf.aspectmodel.shacl.violation.EvaluationContext;
4843
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
4944

45+
import com.google.common.collect.Streams;
46+
5047
/**
5148
* Implementation of a SHACL engine that allows validation on a per-element basis: {@link #validateElement(Resource)} can be used to retrieve validation
5249
* results only for this specific resource.
5350
*/
5451
public class ShaclValidator {
55-
private static final Logger LOG = LoggerFactory.getLogger( ShaclValidator.class );
5652
private final List<Shape.Node> shapes;
5753
private final Map<Resource, List<Shape.Node>> shapesWithClassTargets;
5854
private final Model shapesModel;
55+
private final PathNodeRetriever retriever = new PathNodeRetriever();
5956

6057
/**
6158
* Constructor to provide a custom RDF model containing SHACL shapes
@@ -77,7 +74,7 @@ public ShaclValidator( final Model shapesModel ) {
7774

7875
/**
7976
* 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
77+
* If you have more than one element to validate, prefer the method {@link #validateElements(List)} to calling this method in a loop
8178
* for better performance.
8279
* {@link Resource#getModel()} on the element must not return null, i.e., the resource may not be created using
8380
* {@link org.apache.jena.rdf.model.ResourceFactory#createProperty(String)}, but instead must be created via {@link Model#createResource(String)}.
@@ -128,7 +125,8 @@ public List<Violation> validateModel( final VersionedModel model ) {
128125
private Map<Resource, List<Shape.Node>> findSparqlTargets( final Model model ) {
129126
final Map<Resource, List<Shape.Node>> resourceShapes = new HashMap<>();
130127
for ( final Shape.Node shape : targetSparqlShapes() ) {
131-
final List<Resource> shapeTargets = querySparqlTargets( model, shape.attributes().targetSparql().get() );
128+
final List<Resource> shapeTargets = querySparqlTargets( model, shape.attributes().targetSparql().orElseThrow( () ->
129+
new ShaclValidationException( "SPARQL node shape is missing a target SPARQL expression" ) ) );
132130
for ( final Resource node : shapeTargets ) {
133131
addResourceShape( resourceShapes, node, shape );
134132
}
@@ -160,13 +158,12 @@ private List<Resource> querySparqlTargets( final Model model, final Query query
160158
}
161159

162160
public List<Violation> validateElements( final List<Resource> elements ) {
163-
final Map<Resource, List<Shape.Node>> sparqlTargets = elements.size() > 0 ? findSparqlTargets( elements.get( 0 ).getModel() ) : Map.of();
161+
final Map<Resource, List<Shape.Node>> sparqlTargets = !elements.isEmpty() ? findSparqlTargets( elements.get( 0 ).getModel() ) : Map.of();
164162
return elements.stream().flatMap( element -> validateElement( element, sparqlTargets, element.getModel() ).stream() ).toList();
165163
}
166164

167165
public List<Violation> validateShapeForElement( final Resource element, final Shape.Node shape, final Model resolvedModel ) {
168166
final List<Violation> violations = new ArrayList<>();
169-
final PathNodeRetriever retriever = new PathNodeRetriever();
170167
for ( final Shape.Property property : shape.properties() ) {
171168
for ( final Constraint constraint : property.attributes().constraints() ) {
172169
final List<Statement> reachableNodes = property.path().accept( element, retriever );
@@ -186,7 +183,7 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
186183

187184
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints >= 1
188185
// exists, a violation must be emitted even though no value for the property exists
189-
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint && property.path() instanceof PredicatePath predicatePath ) {
186+
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint && property.path() instanceof final PredicatePath predicatePath ) {
190187
final Property rdfProperty = resolvedModel.createProperty( predicatePath.predicate().getURI() );
191188
final EvaluationContext context = new EvaluationContext( element, shape, Optional.of( rdfProperty ), List.of(), this, resolvedModel );
192189
violations.addAll( constraint.apply( null, context ) );
@@ -257,4 +254,8 @@ public List<Shape.Node> getShapes() {
257254
public Model getShapesModel() {
258255
return shapesModel;
259256
}
257+
258+
public PathNodeRetriever getPathNodeRetriever() {
259+
return retriever;
260+
}
260261
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ record Node(
7979
}
8080

8181
/**
82-
* Implements <code>sh:property</code>
82+
* Implements {@code sh:property}
8383
*/
8484
record Property(
8585
Attributes attributes,

0 commit comments

Comments
 (0)