All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jena.shacl.parser.ShapesParser Maven / Gradle / Ivy

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.parser;

import static org.apache.jena.shacl.engine.ShaclPaths.pathToString;
import static org.apache.jena.shacl.lib.ShLib.displayStr;
import static org.apache.jena.shacl.sys.C.rdfsClass;
import static org.apache.jena.system.G.*;

import java.util.*;
import java.util.stream.Collectors;

import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.atlas.lib.Pair;
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.Imports;
import org.apache.jena.shacl.Shapes;
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.engine.constraint.JLogConstraint;
import org.apache.jena.shacl.lib.ShLib;
import org.apache.jena.shacl.sys.ShaclSystem;
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.system.G;
import org.apache.jena.system.RDFDataException;
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 shapesMap;
    }

    private static final boolean DEBUG = false;
    private static IndentedWriter OUT = IndentedWriter.stdout;

    /**
     * Parse, starting from the given targets.
     * 

* Applications should call functions in {@link Shapes} rather than call the parser directly. */ public static Map parseShapes(Graph shapesGraph, Targets targets) { ConstraintComponents sparqlConstraintComponents = ConstraintComponents.parseSparqlConstraintComponents(shapesGraph); TargetExtensions targetExtensions = TargetExtensions.parseSPARQLTargetType(shapesGraph); return parseShapes(shapesGraph, targets, sparqlConstraintComponents, targetExtensions); } /** Find all names for shapes with explicit type NodeShape or PropertyShape. */ public static Collection findDeclaredShapes(Graph shapesGraph) { Set declared = new HashSet<>(); G.allNodesOfTypeRDFS(shapesGraph, SHACL.NodeShape).forEach(declared::add); G.allNodesOfTypeRDFS(shapesGraph, SHACL.PropertyShape).forEach(declared::add); return declared; } /* The parsing process. *

* Applications should call functions in {@link Shapes} rather than call the parser directly. */ public static ParserResult parseProcess(Graph shapesGraph, Collection declaredNodes) { Targets targets = ShapesParser.targets(shapesGraph); ConstraintComponents sparqlConstraintComponents = ConstraintComponents.parseSparqlConstraintComponents(shapesGraph); TargetExtensions targetExtensions = TargetExtensions.parseSPARQLTargetType(shapesGraph); // Parse from the targets. Map shapesMap = ShapesParser.parseShapes(shapesGraph, targets, sparqlConstraintComponents, targetExtensions); // Root shapes, after parsing from the targets. Collection rootShapes = shapesMap.values().stream() .filter(sh->sh.hasTarget()) .collect(Collectors.toUnmodifiableList()); // Parse non-target shapes - i.e. addressable node and property shapes without // target, with type of sh:NodeShape or sh:PropertyShape // if they were not reached when parsing from the targets. // This skips declared+targets because the shapesMap is in common. Collection declShapes = new ArrayList<>(); declaredNodes.forEach(shapeNode -> { if ( !shapesMap.containsKey(shapeNode) ) { Shape shape = ShapesParser.parseShape(shapesMap, shapesGraph, shapeNode); declShapes.add(shape); } }); // Implicit shapes. // Ones not declared with rdf:type sh:NodeShape or sh:PropertyShape // and not reachable from a target. // Placeholder for a possible feature. Set otherShapes = Set.of(); // Check. // rootShapes and declShapes are disjoint. if ( true ) { rootShapes.forEach(sh->{ if ( declShapes.contains(sh) ) System.err.println("Shape in both: "+sh); if ( ! sh.hasTarget() ) System.err.println("Root shape with no target: "+sh); }); declShapes.forEach(sh->{ if ( sh.hasTarget() ) System.err.println("Declared shape with target: "+sh); }); } // Extract base and imports. Pair> pair = Imports.baseAndImports(shapesGraph); Node shapesBase = pair.getLeft(); List imports = pair.getRight(); ParserResult x = new ParserResult(shapesBase, imports, shapesMap, targets, rootShapes, declShapes, otherShapes, sparqlConstraintComponents, targetExtensions); return x; } private static Map parseShapes(Graph shapesGraph, Targets targets, ConstraintComponents sparqlConstraintComponents, TargetExtensions targetExtensions) { Map shapesMap = new HashMap<>(); // LinkedHashSet - convenience, so shapes are kept in // the order of discovery below. // Record all added by name. // If a shape has two targets, include once. // If a shape with target that refers to another shape with target, include both shapes. // We want both as top-level shapes. Map acc = new LinkedHashMap<>(); if ( DEBUG ) OUT.println("sh:targetNodes"); for ( Node shapeNode : targets.targetNodes ) { parseShapeAcc(acc, shapesMap, shapesGraph, shapeNode); } if ( DEBUG ) OUT.println("sh:targetClasses"); for ( Node shapeNode : targets.targetClasses ) { parseShapeAcc(acc, shapesMap, shapesGraph, shapeNode); } if ( DEBUG ) OUT.println("targetObjectsOf"); for ( Node shapeNode : targets.targetObjectsOf ) { parseShapeAcc(acc, shapesMap, shapesGraph, shapeNode); } if ( DEBUG ) OUT.println("targetSubjectsOf"); for ( Node shapeNode : targets.targetSubjectsOf ) { parseShapeAcc(acc, shapesMap, shapesGraph, shapeNode); } if ( DEBUG ) OUT.println("implicitClassTargets"); for ( Node shapeNode : targets.implicitClassTargets ) { parseShapeAcc(acc, shapesMap, shapesGraph, shapeNode); } if ( DEBUG ) OUT.println("sh:target"); for ( Node shapeNode : targets.targetExtension ) { parseShapeAcc(acc, shapesMap, shapesGraph, shapeNode); } if ( DEBUG ) OUT.println("ConstraintComponents"); // Elsewhere? Keep the parser simply parsing? // Same for targets? Currently done at eval time. if ( sparqlConstraintComponents != null && ! sparqlConstraintComponents.hasParameters() ) { // Deep for all shapes. // Convert all SPARQL constraint components to SPARQL components. shapesMap.values().forEach(shape->{ List 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 return shapesMap; } /** * Parse shape "shNode". If it has been parsed before, simply return. * If not, parse, adding to the overall map of parsed shapes * "parsed" and also add it to the accumulator map. *

* Used by the targets stage. */ private static Shape parseShapeAcc(Map acc, Map shapesMap, Graph shapesGraph, Node shNode) { if ( acc.containsKey(shNode) ) return acc.get(shNode); if ( DEBUG ) OUT.incIndent(); Shape shape = parseShape(shapesMap, shapesGraph, shNode); if ( acc != null ) acc.put(shNode, shape); if ( DEBUG ) OUT.decIndent(); return shape; } // ---- Main parser worker. /** * Parse one shape, updating the record of shapes already parsed. */ public static Shape parseShape(Map shapesMap, Graph shapesGraph, Node shNode) { Set traversed = new HashSet<>(); Shape shape = parseShapeStep(traversed, shapesMap, shapesGraph, shNode); return shape; } // /** Parse a specific shape from the Shapes graph */ // private static Shape parseShape(Graph shapesGraph, Node shNode) { // // Avoid recursion. // Map 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 | ------------------- */ /** Do nothing placeholder shape. */ static Shape unshape(Graph shapesGraph, Node shapeNode) { return new NodeShape(shapesGraph, shapeNode, false, Severity.Violation, Collections.emptySet(), Collections.emptySet(), Collections.singleton(new JLogConstraint("Cycle")), Collections.emptySet()); } /** parse a shape during a parsing process */ /*package*/ static Shape parseShapeStep(Set traversed, Map parsed, Graph shapesGraph, Node shapeNode) { try { // Called by Constraints if ( parsed.containsKey(shapeNode) ) return parsed.get(shapeNode); // Loop detection. Do before parsing. if ( traversed.contains(shapeNode) ) { ShaclSystem.shaclSystemLogger.warn("Cycle detected : node "+ShLib.displayStr(shapeNode)); // Put in a substitute shape. return unshape(shapesGraph, shapeNode); } traversed.add(shapeNode); Shape shape = parseShape$(traversed, parsed, shapesGraph, shapeNode); parsed.put(shapeNode, shape); traversed.remove(shapeNode); return shape; } catch (RDFDataException ex) { throw new ShaclParseException(ex.getMessage()); } } private static Shape parseShape$(Set traversed, Map parsed, Graph shapesGraph, Node shapeNode) { if ( DEBUG ) OUT.printf("Parse shape : %s\n", displayStr(shapeNode)); boolean isDeactivated = contains(shapesGraph, shapeNode, SHACL.deactivated, NodeConst.nodeTrue); Collection targets = targets(shapesGraph, shapeNode); List constraints = Constraints.parseConstraints(shapesGraph, shapeNode, parsed, traversed); Severity severity = severity(shapesGraph, shapeNode); List messages = listSP(shapesGraph, shapeNode, SHACL.message); if ( DEBUG ) OUT.incIndent(); // sh:Property PropertyShapes from this shape. // sh:node is treated as a constraint. List propertyShapes = findPropertyShapes(traversed, 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 names = listSP(shapesGraph, shapeNode, SHACL.name); List descriptions = listSP(shapesGraph, shapeNode, SHACL.description); List 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 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 findPropertyShapes(Set traversed, Map parsed, Graph shapesGraph, Node shapeNode) { List propertyTriples = G.find(shapesGraph, shapeNode, SHACL.property, null).toList(); List 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 paths = listSP(shapesGraph, propertyShape, SHACL.path); throw new ShaclParseException("Multiple sh:path on a property shape: "+displayStr(shapeNode)+" sh:property"+displayStr(propertyShape)+ " : "+paths); } PropertyShape ps = (PropertyShape)parseShapeStep(traversed, parsed, shapesGraph, propertyShape); propertyShapes.add(ps); } return propertyShapes; } private static Collection targets(Graph shapesGraph, Node shape) { //sh:targetClass : rdfs:Class //sh:targetNode : any IRI or literal //sh:targetObjectsOf : rdf:Property //sh:targetSubjectsOf : rdf:Property //sh:target : uses object for further description. List 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); // SHACL-AF accTarget(x, shapesGraph, shape, TargetType.targetExtension); // 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(shape, TargetType.implicitClass, shape, null)); 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 acc, Graph shapesGraph, Node shape, TargetType targetType) { ExtendedIterator iter = shapesGraph.find(shape, targetType.predicate, null); Graph graph; switch(targetType) { // Java 14 has "->" case targetExtension: graph = shapesGraph; break; // These do not need access to the shapes graph so don't force holding on the the graph reference. case implicitClass : case targetClass : case targetNode : case targetObjectsOf : case targetSubjectsOf : default : graph = null; break; } try { iter.mapWith(triple->Target.create(triple.getSubject(), targetType, triple.getObject(), graph)) .forEachRemaining(target->acc.add(target)); } finally { iter.close(); } } public static class ParserResult { public final Node shapesBase; public final List imports; public final Map shapesMap; public final Targets targets; public final Collection rootShapes; public final Collection declaredShapes; public final Collection otherShapes; public final ConstraintComponents sparqlConstraintComponents; public final TargetExtensions targetExtensions; ParserResult(Node shapesBase, List imports, Map shapesMap, Targets targets, Collection rootShapes, Collection declaredShapes, Collection otherShapes, ConstraintComponents sparqlConstraintComponents, TargetExtensions targetExtensions) { this.shapesBase = shapesBase; this.imports = imports; this.shapesMap = shapesMap; this.targets = targets; this.rootShapes = rootShapes; this.declaredShapes = declaredShapes; this.otherShapes = otherShapes; this.sparqlConstraintComponents = sparqlConstraintComponents; this.targetExtensions = targetExtensions; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy