Skip to content

Commit b26adad

Browse files
committed
Fix violation reporting for sh:xone constraints
1 parent 44e5713 commit b26adad

File tree

5 files changed

+146
-19
lines changed

5 files changed

+146
-19
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,12 @@ public List<Violation> validateShapeForElement( final Resource element, final Sh
214214

215215
// MinCount needs to be handled separately: If the property is not used at all on the target node, but a MinCount constraints
216216
// >= 1 exists, a violation must be emitted even though no value for the property exists
217-
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint
218-
&& propertyShape.path() instanceof final PredicatePath predicatePath ) {
219-
final Property rdfProperty = resolvedModel.createProperty( predicatePath.predicate().getURI() );
217+
if ( reachableNodes.isEmpty() && constraint instanceof MinCountConstraint ) {
218+
final Optional<Property> property = propertyShape.path() instanceof PredicatePath ?
219+
Optional.of( resolvedModel.createProperty( ((PredicatePath) propertyShape.path()).predicate().getURI() ) ) :
220+
Optional.empty();
220221
final EvaluationContext context = new EvaluationContext( element, nodeShape, Optional.of( propertyShape ),
221-
Optional.of( rdfProperty ), parentContext, List.of(), this, resolvedModel );
222+
property, parentContext, List.of(), this, resolvedModel );
222223
violations.addAll( constraint.apply( null, context ) );
223224
}
224225
}

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,7 @@ protected AbstractLogicalConstraint( final List<Shape> shapes ) {
3333
}
3434

3535
protected List<List<Violation>> violationsPerShape( final RDFNode rdfNode, final EvaluationContext context ) {
36-
return shapes.stream().map( shape -> {
37-
final EvaluationContext newContext = new EvaluationContext( context.element(), context.shape(),
38-
shape instanceof Shape.Node ? context.propertyShape() : Optional.of( (Shape.Property) shape ),
39-
context.property(), Optional.of( context ), context.offendingStatements(), context.validator(), context.resolvedModel() );
40-
return shape.attributes().constraints().stream().flatMap( constraint -> constraint.apply( rdfNode, newContext ).stream() )
41-
.toList();
42-
} ).toList();
36+
return shapes.stream().map( shape -> shape.accept( new ViolationsForShape( rdfNode, context ) ) ).toList();
4337
}
4438

4539
protected long numberOfEmptyViolationLists( final List<List<Violation>> violationsPerConstraint ) {
@@ -49,4 +43,30 @@ protected long numberOfEmptyViolationLists( final List<List<Violation>> violatio
4943
public List<Shape> shapes() {
5044
return shapes;
5145
}
46+
47+
protected static class ViolationsForShape implements Shape.Visitor<List<Violation>> {
48+
private final RDFNode rdfNode;
49+
private final EvaluationContext context;
50+
51+
protected ViolationsForShape( final RDFNode rdfNode, final EvaluationContext context ) {
52+
this.rdfNode = rdfNode;
53+
this.context = context;
54+
}
55+
56+
@Override
57+
public List<Violation> visitNodeShape( final Shape.Node shape ) {
58+
final EvaluationContext newContext = new EvaluationContext( context.element(), context.shape(),
59+
context.propertyShape(), context.property(), Optional.of( context ), context.offendingStatements(), context.validator(),
60+
context.resolvedModel() );
61+
return shape.attributes().constraints().stream().flatMap( constraint ->
62+
constraint.apply( rdfNode, newContext ).stream() )
63+
.toList();
64+
}
65+
66+
@Override
67+
public List<Violation> visitPropertyShape( final Shape.Property propertyShape ) {
68+
return context.validator().validateShapeForElement( context.element(), (Shape.Node) context.shape(), propertyShape,
69+
context.resolvedModel(), Optional.of( context ) );
70+
}
71+
}
5272
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public boolean canBeUsedOnNodeShapes() {
3535

3636
@Override
3737
public List<Violation> apply( final RDFNode rdfNode, final EvaluationContext context ) {
38-
if ( context.property().isEmpty() ) {
38+
if ( context.propertyShape().isEmpty() ) {
3939
return List.of();
4040
}
4141
if ( minCount > 0 && rdfNode == null ) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ public String parentElementName() {
8383
}
8484

8585
public String propertyName() {
86-
return property().map( Resource::getURI ).map( this::shortUri ).orElse( elementName() );
86+
return property().map( Resource::getURI ).map( this::shortUri )
87+
.orElse( propertyShape().map( propertyShape -> propertyShape.path().toString() ).orElse( elementName() ) );
8788
}
8889

8990
public String parentPropertyName() {

core/esmf-aspect-model-validator/src/test/java/org/eclipse/esmf/aspectmodel/shacl/ShaclValidatorTest.java

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@
1515

1616
import static org.assertj.core.api.Assertions.assertThat;
1717
import static org.junit.jupiter.api.Assertions.assertTrue;
18-
import static org.junit.jupiter.api.Assertions.fail;
1918

2019
import java.io.ByteArrayInputStream;
21-
import java.io.File;
22-
import java.io.FileInputStream;
23-
import java.io.IOException;
2420
import java.io.InputStream;
2521
import java.math.BigInteger;
2622
import java.nio.charset.StandardCharsets;
@@ -31,6 +27,7 @@
3127
import org.eclipse.esmf.aspectmodel.shacl.constraint.MinCountConstraint;
3228
import org.eclipse.esmf.aspectmodel.shacl.constraint.NodeKindConstraint;
3329
import org.eclipse.esmf.aspectmodel.shacl.path.PredicatePath;
30+
import org.eclipse.esmf.aspectmodel.shacl.path.SequencePath;
3431
import org.eclipse.esmf.aspectmodel.shacl.violation.ClassTypeViolation;
3532
import org.eclipse.esmf.aspectmodel.shacl.violation.ClosedViolation;
3633
import org.eclipse.esmf.aspectmodel.shacl.violation.DatatypeViolation;
@@ -1838,7 +1835,7 @@ public void testOrConstraint() {
18381835
}
18391836

18401837
@Test
1841-
public void testXoneConstraint() {
1838+
public void testXoneConstraintInPropertyShape() {
18421839
final Model shapesModel = model( """
18431840
@prefix sh: <http://www.w3.org/ns/shacl#> .
18441841
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@@ -1888,7 +1885,7 @@ public void testXoneConstraint() {
18881885
}
18891886

18901887
@Test
1891-
void testXoneConstraintWithNoSubViolations() {
1888+
void testXoneConstraintInPropertyShapeWithNoSubViolations() {
18921889
final Model shapesModel = model( """
18931890
@prefix sh: <http://www.w3.org/ns/shacl#> .
18941891
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@@ -1939,6 +1936,114 @@ void testXoneConstraintWithNoSubViolations() {
19391936
"Exactly one of the following conditions should lead to a violation, but all of them passed successfully" );
19401937
}
19411938

1939+
@Test
1940+
void testXoneConstraintInNodeShapeExpectSuccess() {
1941+
final Model shapesModel = model( """
1942+
@prefix sh: <http://www.w3.org/ns/shacl#> .
1943+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1944+
@prefix : <http://example.com#> .
1945+
1946+
:MyShape
1947+
a sh:NodeShape ;
1948+
sh:targetClass :TestClass ;
1949+
sh:name "Test shape" ;
1950+
sh:description "Test shape description" ;
1951+
sh:xone (
1952+
:Property1Shape
1953+
:Property2Shape
1954+
) .
1955+
1956+
:Property1Shape
1957+
a sh:PropertyShape ;
1958+
sh:path :foo ;
1959+
sh:minCount 1 .
1960+
1961+
:Property2Shape
1962+
a sh:PropertyShape ;
1963+
sh:path :bar ;
1964+
sh:minCount 1 .
1965+
""" );
1966+
1967+
final Model dataModel = model( """
1968+
@prefix : <http://example.com#> .
1969+
:Foo a :TestClass ;
1970+
:foo 1 .
1971+
""" );
1972+
1973+
final ShaclValidator validator = new ShaclValidator( shapesModel );
1974+
final Resource element = dataModel.createResource( namespace + "Foo" );
1975+
final List<Violation> violations = validator.validateElement( element );
1976+
1977+
assertThat( violations ).isEmpty();
1978+
}
1979+
1980+
@Test
1981+
void testXoneConstraintInNodeShapeExpectFailure() {
1982+
final Model shapesModel = model(
1983+
"""
1984+
@prefix sh: <http://www.w3.org/ns/shacl#> .
1985+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1986+
@prefix : <http://example.com#> .
1987+
1988+
:MyShape
1989+
a sh:NodeShape ;
1990+
sh:targetClass :TestClass ;
1991+
sh:name "Test shape" ;
1992+
sh:description "Test shape description" ;
1993+
sh:xone (
1994+
:Property1Shape
1995+
:Property2Shape
1996+
) .
1997+
1998+
:Property1Shape
1999+
a sh:PropertyShape ;
2000+
sh:path :foo ;
2001+
sh:minCount 1 .
2002+
2003+
:Property2Shape
2004+
a sh:PropertyShape ;
2005+
sh:path ( :bar :baz ) ;
2006+
sh:minCount 1 .
2007+
"""
2008+
);
2009+
2010+
final Model dataModel = model( """
2011+
@prefix : <http://example.com#> .
2012+
:Foo a :TestClass ;
2013+
:testProperty 42 .
2014+
""" );
2015+
2016+
final ShaclValidator validator = new ShaclValidator( shapesModel );
2017+
final Resource element = dataModel.createResource( namespace + "Foo" );
2018+
final List<Violation> violations = validator.validateElement( element );
2019+
2020+
assertThat( violations.size() ).isEqualTo( 1 );
2021+
final Violation finding = violations.get( 0 );
2022+
assertThat( finding ).isInstanceOf( XoneViolation.class );
2023+
assertThat( finding.message() ).startsWith( "Exactly one of the following violations must be fixed:" );
2024+
assertThat( finding.message() ).contains( "Mandatory property :foo is missing on :Foo" );
2025+
assertThat( finding.message() ).contains( "Mandatory property :bar/:baz is missing on :Foo" );
2026+
assertThat( finding ).isInstanceOfSatisfying( XoneViolation.class, xoneViolation ->
2027+
assertThat( xoneViolation.violations() ).satisfiesExactly(
2028+
violation ->
2029+
assertThat( violation ).isInstanceOfSatisfying( MinCountViolation.class, minCountViolation -> {
2030+
assertThat( minCountViolation.allowed() ).isEqualTo( 1 );
2031+
assertThat( minCountViolation.actual() ).isEqualTo( 0 );
2032+
assertThat( minCountViolation.context().propertyShape() ).hasValueSatisfying( property ->
2033+
assertThat( property.path() ).isInstanceOfSatisfying( PredicatePath.class, predicatePath ->
2034+
assertThat( predicatePath.toString() ).isEqualTo( ":foo" ) ) );
2035+
} ),
2036+
violation ->
2037+
assertThat( violation ).isInstanceOfSatisfying( MinCountViolation.class, minCountViolation -> {
2038+
assertThat( minCountViolation.allowed() ).isEqualTo( 1 );
2039+
assertThat( minCountViolation.actual() ).isEqualTo( 0 );
2040+
assertThat( minCountViolation.context().propertyShape() ).hasValueSatisfying( property ->
2041+
assertThat( property.path() ).isInstanceOfSatisfying( SequencePath.class, sequencePath ->
2042+
assertThat( sequencePath.toString() ).isEqualTo( ":bar/:baz" ) ) );
2043+
} )
2044+
) );
2045+
}
2046+
19422047
@Test
19432048
void testSparqlTargetWithGenericConstraint() {
19442049
final Model shapesModel = model( """

0 commit comments

Comments
 (0)