diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclSparqlConstraintFailureException.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclSparqlConstraintFailureException.java new file mode 100644 index 00000000000..90e60aa5035 --- /dev/null +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclSparqlConstraintFailureException.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + ******************************************************************************/ + +package org.eclipse.rdf4j.sail.shacl.ast; + +import java.util.Arrays; + +import org.eclipse.rdf4j.common.exception.RDF4JException; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.query.BindingSet; + +/** + * An exception thrown when the ?failure var is true for a SPARQL constraint select query. + */ +public class ShaclSparqlConstraintFailureException extends RDF4JException { + + private final Shape shape; + private final String query; + private final BindingSet resultBindingSet; + private final Value focusNode; + private final Resource[] dataGraph; + + public ShaclSparqlConstraintFailureException(Shape shape, String query, BindingSet resultBindingSet, + Value focusNode, Resource[] dataGraph) { + super("The ?failure variable was true for " + valueToString(focusNode) + " in shape " + + resourceToString(shape.getId()) + " with result resultBindingSet: " + resultBindingSet.toString() + + " and dataGraph: " + Arrays.toString(dataGraph) + " and query:" + query); + this.shape = shape; + this.query = query; + this.resultBindingSet = resultBindingSet; + this.focusNode = focusNode; + this.dataGraph = dataGraph; + } + + public String getShape() { + return shape.toString(); + } + + public String getQuery() { + return query; + } + + public BindingSet getResultBindingSet() { + return resultBindingSet; + } + + public Value getFocusNode() { + return focusNode; + } + + public Resource[] getDataGraph() { + return dataGraph; + } + + private static String resourceToString(Resource id) { + assert id != null; + if (id == null) { + return "null"; + } + if (id.isIRI()) { + return "<" + id.stringValue() + ">"; + } + if (id.isBNode()) { + return id.toString(); + } + if (id.isTriple()) { + return "TRIPLE " + id; + } + return id.toString(); + } + + private static String valueToString(Value value) { + assert value != null; + if (value == null) { + return "null"; + } + if (value.isResource()) { + return resourceToString((Resource) value); + } + if (value.isLiteral()) { + return value.toString(); + } + return value.toString(); + } +} diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java index 9c8c264d6e7..7280acd8067 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java @@ -97,7 +97,7 @@ abstract public class Shape implements ConstraintComponent, Identifiable { private static final Logger logger = LoggerFactory.getLogger(Shape.class); protected boolean produceValidationReports; - Resource id; + private Resource id; TargetChain targetChain; List target = new ArrayList<>(); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java index 76f30788104..97f22897ac8 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java @@ -17,6 +17,7 @@ import org.eclipse.rdf4j.common.iteration.CloseableIteration; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.impl.BooleanLiteral; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.query.Dataset; import org.eclipse.rdf4j.query.MalformedQueryException; @@ -28,8 +29,10 @@ import org.eclipse.rdf4j.sail.SailConnection; import org.eclipse.rdf4j.sail.SailException; import org.eclipse.rdf4j.sail.memory.MemoryStoreConnection; +import org.eclipse.rdf4j.sail.shacl.ast.ShaclSparqlConstraintFailureException; import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.SparqlConstraintComponent; import org.eclipse.rdf4j.sail.shacl.results.ValidationResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,7 +50,7 @@ public class SparqlConstraintSelect implements PlanNode { private final String query; private final Resource[] dataGraph; private final boolean produceValidationReports; - private final ConstraintComponent constraintComponent; + private final SparqlConstraintComponent constraintComponent; private final Shape shape; private final String[] variables; private final ConstraintComponent.Scope scope; @@ -58,7 +61,7 @@ public class SparqlConstraintSelect implements PlanNode { public SparqlConstraintSelect(SailConnection connection, PlanNode targets, String query, ConstraintComponent.Scope scope, - Resource[] dataGraph, boolean produceValidationReports, ConstraintComponent constraintComponent, + Resource[] dataGraph, boolean produceValidationReports, SparqlConstraintComponent constraintComponent, Shape shape) { this.connection = connection; this.targets = targets; @@ -113,6 +116,13 @@ private void calculateNext() { if (results.hasNext()) { BindingSet bindingSet = results.next(); + if (bindingSet.hasBinding("failure")) { + if (bindingSet.getValue("failure").equals(BooleanLiteral.TRUE)) { + throw new ShaclSparqlConstraintFailureException(shape, query, bindingSet, + nextTarget.getActiveTarget(), dataGraph); + } + } + Value value = bindingSet.getValue("value"); Value path = bindingSet.getValue("path"); diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/SparqlConstraintTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/SparqlConstraintTest.java new file mode 100644 index 00000000000..1f8c1ca3896 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/SparqlConstraintTest.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + ******************************************************************************/ + +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.io.StringReader; + +import org.eclipse.rdf4j.query.Update; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SparqlConstraintTest { + + @Test + public void testFailureBinding() throws IOException { + + SailRepository sailRepository = new SailRepository(new ShaclSail(new MemoryStore())); + + try (SailRepositoryConnection connection = sailRepository.getConnection()) { + connection.add(new StringReader("" + + "@prefix : .\n" + + "@prefix ont: .\n" + + "@prefix vocsh: .\n" + + "@prefix so: .\n" + + "@prefix affected: .\n" + + "@prefix res: .\n" + + "@prefix dct: .\n" + + "@prefix gn: .\n" + + "@prefix owl: .\n" + + "@prefix puml: .\n" + + "@prefix rdf: .\n" + + "@prefix rdfs: .\n" + + "@prefix skos: .\n" + + "@prefix void: .\n" + + "@prefix wgs84: .\n" + + "@prefix xsd: .\n" + + "@prefix sh: .\n" + + "@prefix dash: .\n" + + "@prefix rsx: .\n" + + "@prefix ec: .\n" + + "@prefix ecinst: .\n" + + "@prefix rdf4j: .\n" + + "@prefix ex: .\n" + + "\n" + + "rdf4j:SHACLShapeGraph {\n" + + "\n" + + "ex:\n" + + "\tsh:declare [\n" + + "\t\tsh:prefix \"ex\" ;\n" + + "\t\tsh:namespace \"http://example.com/ns#\"^^xsd:anyURI ;\n" + + "\t] ;\n" + + "\tsh:declare [\n" + + "\t\tsh:prefix \"schema\" ;\n" + + "\t\tsh:namespace \"http://schema.org/\"^^xsd:anyURI ;\n" + + "\t] .\n" + + "\n" + + " ex:LanguageExampleShape\n" + + " \ta sh:NodeShape ;\n" + + " \tsh:targetClass ex:Country ;\n" + + " \tsh:sparql [\n" + + " \t\ta sh:SPARQLConstraint ; # This triple is optional\n" + + " \t\tsh:message \"Values are literals with German language tag.\" ;\n" + + " \t\tsh:prefixes ex: ;\n" + + " \t\tsh:deactivated false ;\n" + + " \t\tsh:select \"\"\"\n" + + " \t\t\tSELECT $this (ex:germanLabel AS ?path) ?value ?failure\n" + + " \t\t\tWHERE {\n" + + " \t\t\t\t$this ex:germanLabel ?value .\n" + + " \t\t\t\tBIND(isIri(?value) as ?failure)\n" + + " \t\t\t}\n" + + " \t\t\t\"\"\" ;\n" + + " \t] .\n" + + "}\n"), RDFFormat.TRIG); + + Update update = connection.prepareUpdate("PREFIX ex: \n" + + "PREFIX owl: \n" + + "PREFIX rdf: \n" + + "PREFIX rdfs: \n" + + "PREFIX sh: \n" + + "PREFIX xsd: \n" + + "\n" + + "INSERT DATA {\n" + + "ex:InvalidCountry a ex:Country .\n" + + "ex:InvalidCountry ex:germanLabel ex:invalidValue .\n" + + "}\n"); + + // assert exception is thrown + RepositoryException repositoryException = assertThrows(RepositoryException.class, update::execute); + Throwable cause = repositoryException.getCause().getCause(); + Assertions.assertEquals( + "org.eclipse.rdf4j.sail.shacl.ast.ShaclSparqlConstraintFailureException: The ?failure variable was true for in shape with result resultBindingSet: [this=http://example.com/ns#InvalidCountry;value=http://example.com/ns#invalidValue;failure=\"true\"^^;path=http://example.com/ns#germanLabel] and dataGraph: [] and query:PREFIX schema: \n" + + + "PREFIX ex: \n" + + "\n" + + "\n" + + "\n" + + " \t\t\tSELECT $this (ex:germanLabel AS ?path) ?value ?failure\n" + + " \t\t\tWHERE {\n" + + " \t\t\t\t$this ex:germanLabel ?value .\n" + + " \t\t\t\tBIND(isIri(?value) as ?failure)\n" + + " \t\t\t}\n" + + " \t\t\t", + cause.toString()); + + } + + } + +}