ValidationQuery.java
/*******************************************************************************
* Copyright (c) 2021 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.sail.SailConnection;
import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable;
import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.EmptyNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.Select;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationTuple;
import org.eclipse.rdf4j.sail.shacl.results.ValidationResult;
public class ValidationQuery {
private final Set<Namespace> namespaces = new HashSet<>();
private ValidationResultGenerator validationResultGenerator;
private String query;
private ConstraintComponent.Scope scope;
private ConstraintComponent.Scope scope_validationReport;
private final List<Variable<Value>> variables;
private int targetIndex;
private int valueIndex;
private boolean propertyShapeWithValue;
private boolean propertyShapeWithValue_validationReport;
private int targetIndex_validationReport;
private int valueIndex_validationReport;
private ConstraintComponent constraintComponent;
private ConstraintComponent constraintComponent_validationReport;
private Severity severity;
private Shape shape;
private List<Variable<?>> extraVariables = List.of();
public ValidationQuery(Collection<Namespace> namespaces, String query, List<Variable<Value>> targets,
Variable<Value> value,
ConstraintComponent.Scope scope, ConstraintComponent constraintComponent, Severity severity,
Shape shape) {
this.namespaces.addAll(namespaces);
this.query = query;
var variables = new ArrayList<>(targets);
if (value != null) {
variables.add(value);
}
this.variables = Collections.unmodifiableList(variables);
if (scope == ConstraintComponent.Scope.propertyShape) {
targetIndex = targets.size() - 1;
if (value != null) {
propertyShapeWithValue = true;
valueIndex = variables.size() - 1;
assert constraintComponent == null
|| constraintComponent.getConstraintComponent().producesValidationResultValue();
} else {
propertyShapeWithValue = false;
valueIndex = variables.size();
assert constraintComponent == null
|| !constraintComponent.getConstraintComponent().alwaysProducesValidationResultValue();
}
} else {
targetIndex = variables.size() - 1;
valueIndex = variables.size() - 1;
}
this.scope = scope;
this.constraintComponent = constraintComponent;
this.severity = severity;
this.shape = shape;
this.validationResultGenerator = new ValidationResultGenerator();
}
public ValidationQuery(Set<Namespace> namespaces, String query, ConstraintComponent.Scope scope,
List<Variable<Value>> variables,
int targetIndex, int valueIndex) {
this.namespaces.addAll(namespaces);
this.query = query;
this.scope = scope;
this.variables = Collections.unmodifiableList(variables);
this.targetIndex = targetIndex;
this.valueIndex = valueIndex;
this.validationResultGenerator = new ValidationResultGenerator();
}
/**
* Creates the SPARQL UNION of two ValidationQuery objects.
*
* @param a The first ValidationQuery.
* @param b The second ValidationQuery.
* @param skipValueCheck Skips checks that the two ValidationQuery object are using the same value. This is useful
* if the ValidationQuery is guaranteed to not use the current value because
* {@link #shiftToNodeShape()} or {@link #popTargetChain()} will always called on the returned
* ValidationQuery
* @return
*/
public static ValidationQuery union(ValidationQuery a, ValidationQuery b, boolean skipValueCheck) {
if (a == Deactivated.instance) {
return b;
}
if (b == Deactivated.instance) {
return a;
}
assert a.getTargetVariable(false).equals(b.getTargetVariable(false));
assert skipValueCheck || !a.propertyShapeWithValue || !b.propertyShapeWithValue
|| a.getValueVariable(false).equals(b.getValueVariable(false));
assert a.scope != ConstraintComponent.Scope.nodeShape || a.valueIndex == a.targetIndex;
assert b.scope != ConstraintComponent.Scope.nodeShape || b.valueIndex == b.targetIndex;
assert a.scope == b.scope;
assert a.targetIndex == b.targetIndex;
assert a.valueIndex == b.valueIndex;
String unionQuery = "{\n" + a.getQuery() + "\n} UNION {\n" + b.query + "\n}";
var variables = a.variables.size() >= b.variables.size() ? a.variables
: b.variables;
Set<Namespace> namespaces = new HashSet<>();
namespaces.addAll(a.namespaces);
namespaces.addAll(b.namespaces);
if (a.propertyShapeWithValue || a.scope == ConstraintComponent.Scope.nodeShape) {
assert a.variables.size() > a.valueIndex;
return new ValidationQuery(namespaces, unionQuery, a.scope, variables.subList(0, a.valueIndex + 1),
a.targetIndex,
a.valueIndex);
} else {
assert a.variables.size() >= a.valueIndex;
return new ValidationQuery(namespaces, unionQuery, a.scope, a.variables.subList(0, a.valueIndex),
a.targetIndex,
a.valueIndex);
}
}
public String getQuery() {
return query;
}
public PlanNode getValidationPlan(SailConnection baseConnection, Resource[] dataGraph,
Resource[] shapesGraphs) {
assert query != null;
assert shape != null;
assert scope_validationReport != null;
String fullQueryString = getFullQueryString();
Select select = new Select(baseConnection, fullQueryString, bindings -> {
var validationResultFunction = validationResultGenerator.getValidationTupleValidationResultFunction(this,
shapesGraphs, bindings);
ValidationTuple validationTuple;
if (scope_validationReport == ConstraintComponent.Scope.propertyShape) {
if (propertyShapeWithValue_validationReport) {
validationTuple = new ValidationTuple(bindings.getValue(getTargetVariable(true)),
bindings.getValue(getValueVariable(true)),
scope_validationReport, true, dataGraph);
} else {
validationTuple = new ValidationTuple(bindings.getValue(getTargetVariable(true)),
scope_validationReport, false, dataGraph);
}
} else {
validationTuple = new ValidationTuple(bindings.getValue(getTargetVariable(true)),
scope_validationReport, true, dataGraph);
}
return validationTuple.addValidationResult(validationResultFunction);
}, dataGraph);
return select;
}
public static class ValidationResultGenerator {
public Function<ValidationTuple, ValidationResult> getValidationTupleValidationResultFunction(
ValidationQuery validationQuery, Resource[] shapesGraphs, BindingSet bindings) {
return t -> new ValidationResult(t.getActiveTarget(), t.getValue(), validationQuery.shape,
validationQuery.constraintComponent_validationReport, validationQuery.severity, t.getScope(),
t.getContexts(), shapesGraphs);
}
}
public void setValidationResultGenerator(List<Variable<?>> extraVariables,
ValidationResultGenerator validationResultGenerator) {
this.validationResultGenerator = validationResultGenerator;
this.extraVariables = extraVariables;
}
private String getFullQueryString() {
String extraVariablesString;
if (!extraVariables.isEmpty()) {
Optional<String> reduce = extraVariables.stream()
.map(Variable::asSparqlVariable)
.reduce((a, b) -> a + " " + b);
if (reduce.isPresent()) {
extraVariablesString = reduce.get() + " ";
} else {
extraVariablesString = "";
}
} else {
extraVariablesString = "";
}
if (scope_validationReport == ConstraintComponent.Scope.propertyShape
&& propertyShapeWithValue_validationReport) {
return ShaclPrefixParser.toSparqlPrefixes(namespaces) + "\nSELECT DISTINCT " +
"?" + getTargetVariable(true) + " " +
"?" + getValueVariable(true) + " " +
extraVariablesString +
"WHERE {\n" + query + "\n}";
} else {
return ShaclPrefixParser.toSparqlPrefixes(namespaces) + "\nSELECT DISTINCT " +
"?" + getTargetVariable(true) + " " +
extraVariablesString +
"WHERE {\n" + query + "\n}";
}
}
private String getValueVariable(boolean forValidationReport) {
if (forValidationReport) {
return variables.get(valueIndex_validationReport).name;
}
assert scope != ConstraintComponent.Scope.propertyShape || propertyShapeWithValue;
return variables.get(valueIndex).name;
}
private String getTargetVariable(boolean forValidationReport) {
if (forValidationReport) {
return variables.get(targetIndex_validationReport).name;
}
return variables.get(targetIndex).name;
}
public ValidationQuery withSeverity(Severity severity) {
this.severity = severity;
return this;
}
public ValidationQuery withShape(Shape shape) {
this.shape = shape;
return this;
}
public void popTargetChain() {
assert scope == ConstraintComponent.Scope.propertyShape;
this.propertyShapeWithValue = true;
targetIndex--;
valueIndex--;
}
public void shiftToNodeShape() {
assert this.scope == ConstraintComponent.Scope.propertyShape;
this.scope = ConstraintComponent.Scope.nodeShape;
this.propertyShapeWithValue = false;
valueIndex--;
}
public void shiftToPropertyShape() {
assert this.scope == ConstraintComponent.Scope.nodeShape;
this.scope = ConstraintComponent.Scope.propertyShape;
this.propertyShapeWithValue = true;
targetIndex--;
}
public ValidationQuery withConstraintComponent(ConstraintComponent constraintComponent) {
this.constraintComponent = constraintComponent;
return this;
}
public void makeCurrentStateValidationReport() {
valueIndex_validationReport = valueIndex;
targetIndex_validationReport = targetIndex;
scope_validationReport = scope;
constraintComponent_validationReport = constraintComponent;
propertyShapeWithValue_validationReport = propertyShapeWithValue;
}
public Shape getShape() {
return shape;
}
public Severity getSeverity() {
return severity;
}
public ConstraintComponent getConstraintComponent_validationReport() {
return constraintComponent_validationReport;
}
// used for sh:deactivated
public static class Deactivated extends ValidationQuery {
private static final Deactivated instance = new Deactivated();
private Deactivated() {
super(List.of(), "", Collections.emptyList(), null, null, null, null, null);
}
public static Deactivated getInstance() {
return instance;
}
@Override
public PlanNode getValidationPlan(SailConnection baseConnection, Resource[] dataGraph,
Resource[] shapesGraphs) {
return EmptyNode.getInstance();
}
@Override
public ValidationQuery withSeverity(Severity severity) {
return this;
}
@Override
public ValidationQuery withShape(Shape shape) {
return this;
}
@Override
public void popTargetChain() {
// no-op;
}
@Override
public void shiftToNodeShape() {
// no-op;
}
@Override
public void shiftToPropertyShape() {
// no-op;
}
@Override
public ValidationQuery withConstraintComponent(ConstraintComponent constraintComponent) {
return this;
}
@Override
public void makeCurrentStateValidationReport() {
// no-op;
}
}
}