org.apache.jena.shacl.engine.constraint.SparqlValidation Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jena-shacl Show documentation
Show all versions of jena-shacl Show documentation
SHACL engine for Apache Jena
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jena.shacl.engine.constraint;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Triple;
import org.apache.jena.query.*;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.riot.out.NodeFmtLib;
import org.apache.jena.shacl.engine.Parameter;
import org.apache.jena.shacl.engine.ShaclPaths;
import org.apache.jena.shacl.engine.ValidationContext;
import org.apache.jena.shacl.lib.ShLib;
import org.apache.jena.shacl.parser.Constraint;
import org.apache.jena.shacl.parser.Shape;
import org.apache.jena.shacl.validation.event.ConstraintEvaluatedOnSinglePathNodeEvent;
import org.apache.jena.sparql.core.*;
import org.apache.jena.sparql.engine.binding.Binding;
import org.apache.jena.sparql.path.P_Link;
import org.apache.jena.sparql.path.Path;
import org.apache.jena.sparql.path.PathFactory;
import org.apache.jena.sparql.syntax.Element;
import org.apache.jena.sparql.syntax.ElementPathBlock;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransformCopyBase;
import org.apache.jena.sparql.syntax.syntaxtransform.QueryTransformOps;
import org.apache.jena.sparql.util.ModelUtils;
/** The SPARQL validator algorithms. */
/*package*/ class SparqlValidation {
private static final boolean USE_QueryTransformOps = false;
public static void validate(ValidationContext vCxt, Graph data, Shape shape,
Node focusNode, Path path, Node valueNode,
Query query, MultiValuedMap parameterMap,
String violationTemplate, Constraint reportConstraint) {
// Two sub-cases:
// Syntax rule: https://www.w3.org/TR/shacl/#syntax-rule-multiple-parameters
// If there are >1 parameters, each must be single valued.
// so:
// Multimap, one parameter, multiple values => conjunction of each, with one report.
// Multimap, any number of parameters, single values => single validation with one report.
if ( parameterMap != null ) {
if ( parameterMap.keySet().size() == 1 && parameterMap.size() > 1 ) {
for ( Entry e : parameterMap.entries()) {
Map pmap = Collections.singletonMap(e.getKey(), e.getValue());
boolean b = validateMap(vCxt, data, shape, focusNode, path, valueNode, query, pmap, violationTemplate, reportConstraint);
if ( ! b )
// Validation error - return early.
return;
}
return;
}
}
// Convert to map.
Map pmap = flatten(parameterMap);
/*boolean b =*/
validateMap(vCxt, data, shape, focusNode, path, valueNode, query, pmap, violationTemplate, reportConstraint);
}
private static Map flatten(MultiValuedMap parameterMap) {
if ( parameterMap == null )
return null;
Map pmap = new HashMap<>(parameterMap.size());
parameterMap.mapIterator().forEachRemaining(k->parameterMap.get(k).forEach(v->pmap.put(k,v)) );
return pmap;
}
/** return true if the validation is "conforms" */
private static boolean validateMap(ValidationContext vCxt, Graph data, Shape shape,
Node focusNode, Path path, Node valueNode,
Query _query, Map parameterMap,
String violationTemplate, Constraint reportConstraint) {
Model model = ModelFactory.createModelForGraph(data);
QueryExecution qExec;
Query query = _query;
// If path is not a simple link, rewrite the query.
if ( path != null && !(path instanceof P_Link ) )
query = QueryTransformOps.transform(query, new ElementTransformPath(SparqlConstraint.varPath, path));
if ( USE_QueryTransformOps ) {
// Done with QueryTransformOps.transform
Map substitutions = parameterMapToSyntaxSubstitutions(parameterMap, focusNode, path);
if ( query.isAskType() )
addSubstition(substitutions, "value", valueNode);
Query query2 = QueryTransformOps.transform(query, substitutions);
qExec = QueryExecutionFactory.create(query2, model);
} else {
// Done with pre-binding.
QuerySolutionMap qsm = parameterMapToPreBinding(parameterMap, focusNode, path, model);
if ( query.isAskType() )
qsm.add("value", ModelUtils.convertGraphNodeToRDFNode(valueNode, model));
//qExec = QueryExecution.create().query(query).model(model).initialBinding(qsm).build();
// ---- Dataset needed for the shapes graph
Resource shapesGraphResource = model.createResource("foo");
qsm.add("currentShape", ModelUtils.convertGraphNodeToRDFNode(shape.getShapeNode(), model));
qsm.add("shapesGraph", shapesGraphResource);
// No copying of graphs. Set the default graph on creation.
DatasetGraph dsg = DatasetGraphFactory.createGeneral(model.getGraph()); // Dataset by links.
dsg.addGraph(shapesGraphResource.asNode(), shape.getShapeGraph());
Dataset ds = DatasetFactory.wrap(dsg);
qExec = QueryExecution.create().query(query).dataset(ds).initialBinding(qsm).build();
}
// ASK validator.
if ( qExec.getQuery().isAskType() ) {
boolean b = qExec.execAsk();
if ( ! b ) {
String msg = ( violationTemplate == null )
? "SPARQL ASK constraint for "+ShLib.displayStr(valueNode)+" returns false"
: substitute(violationTemplate, parameterMap, focusNode, path, valueNode);
vCxt.reportEntry(msg, shape, focusNode, path, valueNode, reportConstraint);
}
vCxt.notifyValidationListener(() ->
new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, reportConstraint, path, valueNode,b));
return b;
}
// SELECT validator.
ResultSet rs = qExec.execSelect();
if ( ! rs.hasNext() ) {
vCxt.notifyValidationListener(() ->
new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, reportConstraint, path, valueNode, true));
return true;
}
while(rs.hasNext()) {
Binding row = rs.nextBinding();
Node value = row.get(SparqlConstraint.varValue);
if ( value == null )
value = valueNode;
String msg;
if ( violationTemplate == null ) {
if ( value != null )
msg = "SPARQL SELECT constraint for "+ShLib.displayStr(valueNode)+" returns "+ShLib.displayStr(value);
else
msg = "SPARQL SELECT constraint for "+ShLib.displayStr(valueNode)+" returns row "+row;
} else {
msg = substitute(violationTemplate, row);
}
Path rPath = path;
if ( rPath == null ) {
Node qPath = row.get(SparqlConstraint.varPath);
if ( qPath != null )
rPath = PathFactory.pathLink(qPath);
}
final Path finalRPath = rPath;
final Node finalValue = value;
vCxt.notifyValidationListener(() ->
new ConstraintEvaluatedOnSinglePathNodeEvent(vCxt, shape, focusNode, reportConstraint, finalRPath, finalValue, false));
vCxt.reportEntry(msg, shape, focusNode, rPath, value, reportConstraint);
}
return false;
}
/** Result message: SELECT substitute */
private static String substitute(String violationTemplate, Binding row) {
String x = violationTemplate;
Iterator iter = row.vars();
while(iter.hasNext()) {
Var var = iter.next();
x = substit(x, var.getVarName(), row.get(var));
}
return x;
}
/** Result message: ASK substitute */
private static String substitute(String violationTemplate, Map parameterMap, Node focusNode, Path path, Node valueNode) {
String x = violationTemplate;
for ( Entry e : parameterMap.entrySet() ) {
x = substit(x, e.getKey().getSparqlName(), e.getValue());
}
return x;
}
/** Substitution */
private static String substit(String x, String name, Node value) {
try {
String vn = "\\{[?$]"+Matcher.quoteReplacement(name)+"\\}";
String val = strQuoted(value);
return x.replaceAll(vn, val);
} catch (RuntimeException ex) {
Log.warn(SparqlValidation.class, "Failed to substitute into string for name="+name+" value="+value);
return x;
}
}
/** regex-safe string */
private static String strQuoted(Node node) {
String x = node.isLiteral()
? node.getLiteralLexicalForm()
: ShLib.displayStr(node);
x = Matcher.quoteReplacement(x);
return x;
}
private static Map parameterMapToSyntaxSubstitutions(Map parameterMap, Node thisNode, Path path) {
Map substitions = parametersToMap(parameterMap, thisNode);
if ( path != null ) {
addSubstition(substitions, "PATH", ShaclPaths.pathNode(path));
}
return substitions;
}
private static Map parametersToMap(Map parameterMap, Node thisNode) {
Map substitions = new HashMap<>();
if ( parameterMap != null ) {
parameterMap.forEach((p,n)-> addSubstition(substitions, p.getSparqlName(), n));
}
addSubstition(substitions, "this", thisNode);
return substitions;
}
private static QuerySolutionMap parameterMapToPreBinding(Map parameterMap, Node thisNode, Path path, Model model) {
QuerySolutionMap qsm = new QuerySolutionMap();
if ( parameterMap != null ) {
parameterMap.forEach((p,n)->
qsm.add(p.getSparqlName(), ModelUtils.convertGraphNodeToRDFNode(n, model)));
}
qsm.add("this", ModelUtils.convertGraphNodeToRDFNode(thisNode, model));
if ( path != null ) {
Node pn = ShaclPaths.pathNode(path);
// If 'path' can not be translated into a substituted form, then ignore
// PATH. This means that is the path is not a simple link, it could only be
// done by textually substitution of the SPARQL query string.
if ( pn != null ) {
RDFNode z = ModelUtils.convertGraphNodeToRDFNode(pn, model);
qsm.add("PATH", z);
}
}
return qsm;
}
private static void addSubstition(Map substitions, String sparqlName, Node n) {
substitions.put(Var.alloc(sparqlName), n);
}
/** ${var} in a string. */
private static String messageTemplate(String message, Map parameterMap, Node thisNode, Path path) {
Map substitions = parametersToMap(parameterMap, thisNode);
Pattern pattern = Pattern.compile("{[$?][^{}]+}");
if ( path != null )
// PATH is special.
substitions.put(Var.alloc("PATH"), NodeFactory.createLiteralString(ShaclPaths.pathToString(path)));
return subsitute(message, substitions);
}
private static Pattern pattern = Pattern.compile("(\\{[\\$\\?][^{}]+\\})");
// String substitution.
private static String subsitute(String string, Map substitions) {
StringBuilder sb = new StringBuilder();
Matcher m = pattern.matcher(string);
int prev = 0 ;
while(m.find()) {
int i1 = m.start();
int i2 = m.end();
String var = m.group();
String varName = var.substring(2, i2-i1-1);
sb.append(string.substring(prev,i1));
Var v = Var.alloc(varName);
Node n = substitions.get(v);
if ( n == null )
sb.append(var);
else {
String z = NodeFmtLib.displayStr(n);
sb.append(z);
}
prev = i2;
}
sb.append(string.substring(prev));
return sb.toString();
}
/** Rewrite a path block, replacing a variable in the predicate slot of a triple with a path. */
private static class ElementTransformPath extends ElementTransformCopyBase {
private final Var var;
private final Path path;
ElementTransformPath(Var varPath, Path path) {
this.var = varPath;
this.path = path;
}
@Override
public Element transform(ElementPathBlock el) {
ElementPathBlock el2 = new ElementPathBlock();
boolean changed = false ;
PathBlock pathBlock = el.getPattern();
List x = pathBlock.getList();
for(TriplePath tp : x ) {
if ( ! tp.isTriple() ) {
el2.addTriple(tp);
continue;
}
Triple t = tp.asTriple();
if ( !( var.equals(t.getPredicate()) ) ) {
el2.addTriple(tp);
continue;
}
TriplePath tp2 = new TriplePath(t.getSubject(), path, t.getObject());
el2.addTriple(tp2);
changed = true ;
}
return changed ? el2 : el ;
}
}
}