blob: 10a0bfc547fd7f858ca616a5a014ea2f3c082dcb [file] [log] [blame]
/*
* 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.parser;
import static org.apache.jena.shacl.engine.ShaclPaths.pathToString;
import static org.apache.jena.shacl.lib.G.*;
import static org.apache.jena.shacl.lib.ShLib.displayStr;
import static org.apache.jena.shacl.sys.C.rdfsClass;
import java.util.*;
import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.datatypes.xsd.XSDDatatype;
import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.shacl.engine.ShaclPaths;
import org.apache.jena.shacl.engine.Target;
import org.apache.jena.shacl.engine.TargetType;
import org.apache.jena.shacl.engine.Targets;
import org.apache.jena.shacl.lib.G;
import org.apache.jena.shacl.validation.Severity;
import org.apache.jena.shacl.vocabulary.SHACL;
import org.apache.jena.shared.JenaException;
import org.apache.jena.sparql.graph.NodeConst;
import org.apache.jena.sparql.path.Path;
import org.apache.jena.util.iterator.ExtendedIterator;
public class ShapesParser {
public static Targets targets(Graph shapesGraph) {
return Targets.create(shapesGraph);
}
static class ParserState {
Targets rootShapes;
ConstraintComponents sparqlConstraintComponents;
Map<Node, Shape> shapesMap;
}
private static final boolean DEBUG = false;
private static IndentedWriter OUT = IndentedWriter.stdout;
//private static Logger LOG = LoggerFactory.getLogger(ShapesParser.class);
/** Return a list of "top shapes", those with targets.
* The {@code shapesMap} is modified, adding in all shapes processed.
*/
public static Collection<Shape> parseShapes(Graph shapesGraph, Targets targets, Map<Node, Shape> shapesMap) {
Targets rootShapes = targets;
if ( DEBUG )
OUT.println("SparqlConstraintComponents");
ConstraintComponents sparqlConstraintComponents = ConstraintComponents.parseSparqlConstraintComponents(shapesGraph);
// LinkedHashMap - convenience, so shapes are kept in
// the order of discovery below.
Map<Node, Shape> acc = new LinkedHashMap<>();
if ( DEBUG )
OUT.println("sh:targetNodes");
for ( Node shapeNode : rootShapes.targetNodes ) {
parseRootShape(acc, shapesMap, shapesGraph, shapeNode);
}
if ( DEBUG )
OUT.println("sh:targetClasses");
for ( Node shapeNode : rootShapes.targetClasses ) {
parseRootShape(acc, shapesMap, shapesGraph, shapeNode);
}
if ( DEBUG )
OUT.println("targetObjectsOf");
for ( Node shapeNode : rootShapes.targetObjectsOf ) {
parseRootShape(acc, shapesMap, shapesGraph, shapeNode);
}
if ( DEBUG )
OUT.println("targetSubjectsOf");
for ( Node shapeNode : rootShapes.targetSubjectsOf ) {
parseRootShape(acc, shapesMap, shapesGraph, shapeNode);
}
if ( DEBUG )
OUT.println("implicitClassTargets");
for ( Node shapeNode : rootShapes.implicitClassTargets ) {
parseRootShape(acc, shapesMap, shapesGraph, shapeNode);
}
if ( DEBUG )
OUT.println("ConstraintComponents");
if ( sparqlConstraintComponents != null && ! sparqlConstraintComponents.isEmpty() ) {
// Deep for all shapes.
shapesMap.values().forEach(shape->{
List<Constraint> x = ConstraintComponents.processShape(shapesGraph, sparqlConstraintComponents, shape);
if ( x != null && !x.isEmpty() ) {
shape.getConstraints().addAll(x);
}
});
}
// Syntax rules for well-formed shapes.
//https://www.w3.org/TR/shacl/#syntax-rules
// Note - we only have the reachable shapes in "shapesMap".
return shapes(acc) ;
}
/** Parse and add all the declared shapes into the map.
* The {@code shapesMap} is modified, adding in all shapes processed.
*/
public static Collection<Shape> declaredShapes(Graph shapesGraph, Map<Node, Shape> shapesMap) {
// All declared shapes.
Map<Node, Shape> acc = new LinkedHashMap<>();
G.listAllNodesOfType(shapesGraph, SHACL.NodeShape).forEach(shapeNode->
parseRootShape(acc, shapesMap, shapesGraph, shapeNode));
G.listAllNodesOfType(shapesGraph, SHACL.PropertyShape).forEach(shapeNode->
parseRootShape(acc, shapesMap, shapesGraph, shapeNode));
return shapes(acc);
}
private static Collection<Shape> shapes(Map<Node, Shape> acc) {
// The list will be in insertion order.
return new ArrayList<>(acc.values());
}
private static void parseRootShape(Map<Node, Shape> acc, Map<Node, Shape> parsed, Graph shapesGraph, Node shNode) {
if ( acc.containsKey(shNode) )
// Already processed as root shape.
return;
if ( DEBUG )
OUT.incIndent();
Shape shape = parseShapeStep(parsed, shapesGraph, shNode);
acc.put(shNode, shape);
if ( DEBUG )
OUT.decIndent();
}
/** Parse a specific shape from the Shapes graph */
public static Shape parseShape(Graph shapesGraph, Node shNode) {
// Avoid recursion.
Map<Node, Shape> parsed = new HashMap<>();
return parseShapeStep(parsed, shapesGraph, shNode);
}
/*
?X rdfs:domain sh:Shape
-----------------------
| X |
=======================
| sh:targetClass |
| sh:targetSubjectsOf |
| sh:targetObjectsOf |
| sh:severity |
| sh:property |
| sh:targetNode |
| sh:sparql |
| sh:target |
| sh:rule |
-----------------------
*/
/*
?X rdfs:domain sh:NodeShape
-----
| X |
=====
-----
*/
/*
?X rdfs:domain sh:PropertyShape
-------------------
| X |
===================
| sh:name |
| sh:group |
| sh:description |
| sh:defaultValue |
| sh:path |
-------------------
*/
/** parse a shape during a parsing process */
/*package*/ static Shape parseShapeStep(Map<Node, Shape> parsed, Graph shapesGraph, Node shapeNode) {
if ( parsed.containsKey(shapeNode) )
return parsed.get(shapeNode);
Shape shape = parseShape$(parsed, shapesGraph, shapeNode);
parsed.put(shapeNode, shape);
return shape;
}
private static Shape parseShape$(Map<Node, Shape> parsed, Graph shapesGraph, Node shapeNode) {
if ( DEBUG )
OUT.printf("Parse shape : %s\n", displayStr(shapeNode));
boolean isDeactivated = absentOrOne(shapesGraph, shapeNode, SHACL.deactivated, NodeConst.nodeTrue);
Collection<Target> targets = targets(shapesGraph, shapeNode);
List<Constraint> constraints = Constraints.parseConstraints(shapesGraph, shapeNode, parsed);
Severity severity = severity(shapesGraph, shapeNode);
List<Node> messages = listSP(shapesGraph, shapeNode, SHACL.message);
if ( DEBUG )
OUT.incIndent();
// sh:Property PropertyShapes from this shape.
// sh:node is treated as a constraint.
List<PropertyShape> propertyShapes = findPropertyShapes(parsed, shapesGraph, shapeNode);
if ( DEBUG )
OUT.decIndent();
boolean isPropertyShape = contains(shapesGraph, shapeNode, SHACL.path, Node.ANY);
if ( ! isPropertyShape ) {
if ( DEBUG )
OUT.printf("Node shape %s\n", displayStr(shapeNode));
return new NodeShape(shapesGraph, shapeNode, isDeactivated, severity, messages, targets, constraints, propertyShapes);
}
// -- Property shape.
if ( DEBUG )
OUT.incIndent();
Node pathNode = getOneSP(shapesGraph, shapeNode, SHACL.path);
Path path = parsePath(shapesGraph, pathNode);
if ( DEBUG )
OUT.printf("Property shape: path = %s\n", pathToString(shapesGraph, path));
// 2.3.2 Non-Validating Property Shape Characteristics
// 2.3.2.1 sh:name and sh:description
// 2.3.2.2 sh:order
// 2.3.2.3 sh:group
// 2.3.2.4 sh:defaultValue
// sh:order and sh:defaultValue - unique.
List<Node> names = listSP(shapesGraph, shapeNode, SHACL.name);
List<Node> descriptions = listSP(shapesGraph, shapeNode, SHACL.description);
List<Node> groups = listSP(shapesGraph, shapeNode, SHACL.group);
Node defaultValue = G.getZeroOrOneSP(shapesGraph, shapeNode, SHACL.defaultValue);
// The values of sh:order are decimals. "maybe used on any type of subject" but section is "property shapes".
// We allow more than decimals.
Node order = G.getZeroOrOneSP(shapesGraph, shapeNode, SHACL.order);
if ( order != null && ! isDecimalCompatible(order) )
throw new ShaclParseException("Not an xsd:decimal for sh:order");
if ( DEBUG ) {
OUT.printf("Property shape %s\n", displayStr(shapeNode));
OUT.decIndent();
}
return new PropertyShape(shapesGraph, shapeNode, isDeactivated, severity, messages, targets, path, constraints, propertyShapes);
}
/* Some of the data types that are derived from xsd:decimal (not all of them) */
private static Set<RDFDatatype> decimalCompatible = new HashSet<>();
static {
decimalCompatible.add(XSDDatatype.XSDdecimal);
decimalCompatible.add(XSDDatatype.XSDinteger);
decimalCompatible.add(XSDDatatype.XSDlong);
decimalCompatible.add(XSDDatatype.XSDint);
}
private static boolean isDecimalCompatible(Node node) {
try {
RDFDatatype dt = node.getLiteralDatatype();
return decimalCompatible.contains(dt);
} catch (JenaException ex) { return false; }
}
private static Path parsePath(Graph shapesGraph, Node node) {
return ShaclPaths.parsePath(shapesGraph, node);
}
private static List<PropertyShape> findPropertyShapes(Map<Node, Shape> parsed, Graph shapesGraph, Node shapeNode) {
List<Triple> propertyTriples = G.find(shapesGraph, shapeNode, SHACL.property, null).toList();
List<PropertyShape> propertyShapes = new ArrayList<>();
for ( Triple t : propertyTriples) {
// Must be a property shape.
Node propertyShape = object(t);
long x = countSP(shapesGraph, propertyShape, SHACL.path);
if ( x == 0 ) {
// Is it a typo? -> Can we find it as a subject?
boolean existsAsSubject = G.contains(shapesGraph, propertyShape, null,null);
if ( ! existsAsSubject )
throw new ShaclParseException("Missing property shape: node="+displayStr(shapeNode)+" sh:property "+displayStr(propertyShape));
else
throw new ShaclParseException("No sh:path on a property shape: node="+displayStr(shapeNode)+" sh:property "+displayStr(propertyShape));
}
if ( x > 1 ) {
List<Node> paths = listSP(shapesGraph, propertyShape, SHACL.path);
throw new ShaclParseException("Muiltiple sh:path on a property shape: "+displayStr(shapeNode)+" sh:property"+displayStr(propertyShape)+ " : "+paths);
}
PropertyShape ps = (PropertyShape)parseShapeStep(parsed, shapesGraph, propertyShape);
propertyShapes.add(ps);
}
return propertyShapes;
}
private static Collection<Target> targets(Graph shapesGraph, Node shape) {
//sh:targetClass : rdfs:Class
//sh:targetNode : any IRI or literal
//sh:targetObjectsOf : rdf:Property
//sh:targetSubjectsOf : rdf:Property
List<Target> x = new ArrayList<>();
accTarget(x, shapesGraph, shape, TargetType.targetNode);
accTarget(x, shapesGraph, shape, TargetType.targetClass);
accTarget(x, shapesGraph, shape, TargetType.targetObjectsOf);
accTarget(x, shapesGraph, shape, TargetType.targetSubjectsOf);
// TargetType.implicitClass : some overlap with TargetOps.implicitClassTargets
// Explicitly sh:NodeShape or sh:PropertyShape and also subClassof* rdfs:Class.
if ( isShapeType(shapesGraph, shape) && isOfType(shapesGraph, shape, rdfsClass) )
x.add(Target.create(TargetType.implicitClass, shape));
return x;
}
private static boolean isShapeType(Graph shapesGraph, Node shape) {
return hasType(shapesGraph, shape, SHACL.NodeShape) || hasType(shapesGraph, shape, SHACL.PropertyShape);
}
private static Severity severity(Graph shapesGraph, Node shNode) {
Node sev = G.getSP(shapesGraph, shNode, SHACL.severity);
if ( sev == null )
return Severity.Violation;
return Severity.create(sev);
}
private static void accTarget(Collection<Target> acc, Graph shapesGraph, Node shape, TargetType targetType) {
ExtendedIterator<Triple> iter = shapesGraph.find(shape, targetType.predicate, null);
try {
iter.mapWith(triple->Target.create(targetType, triple.getObject()))
.forEachRemaining(target->acc.add(target));
} finally { iter.close(); }
}
}