/* * Licensed 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. * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. */ package org.topbraid.shacl.arq; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.apache.jena.graph.Node; import org.apache.jena.query.Query; import org.apache.jena.query.QueryParseException; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.RDFList; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.Statement; import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.sparql.path.P_Alt; import org.apache.jena.sparql.path.P_Inverse; import org.apache.jena.sparql.path.P_Link; import org.apache.jena.sparql.path.P_OneOrMore1; import org.apache.jena.sparql.path.P_OneOrMoreN; import org.apache.jena.sparql.path.P_Path1; import org.apache.jena.sparql.path.P_Seq; import org.apache.jena.sparql.path.P_ZeroOrMore1; import org.apache.jena.sparql.path.P_ZeroOrMoreN; import org.apache.jena.sparql.path.P_ZeroOrOne; import org.apache.jena.sparql.path.Path; import org.apache.jena.sparql.path.eval.PathEval; import org.apache.jena.sparql.syntax.Element; import org.apache.jena.sparql.syntax.ElementGroup; import org.apache.jena.sparql.syntax.ElementPathBlock; import org.apache.jena.sparql.syntax.ElementTriplesBlock; import org.apache.jena.sparql.util.Context; import org.apache.jena.sparql.util.FmtUtils; import org.apache.jena.vocabulary.RDF; import org.topbraid.jenax.util.ARQFactory; import org.topbraid.shacl.vocabulary.SH; /** * Utilties to manage the conversion between SHACL paths and SPARQL 1.1 property paths. * * @author Holger Knublauch */ public class SHACLPaths { private final static String ALTERNATIVE_PATH_SEPARATOR = "|"; private final static String SEQUENCE_PATH_SEPARATOR = "/"; public static void addValueNodes(RDFNode focusNode, Path path, Collection<RDFNode> results) { Set<Node> seen = new HashSet<>(); Iterator<Node> it = PathEval.eval(focusNode.getModel().getGraph(), focusNode.asNode(), path, Context.emptyContext); while(it.hasNext()) { Node node = it.next(); if(!seen.contains(node)) { seen.add(node); results.add(focusNode.getModel().asRDFNode(node)); } } } public static void addValueNodes(RDFNode focusNode, Property predicate, Collection<RDFNode> results) { if(focusNode instanceof Resource) { StmtIterator it = ((Resource)focusNode).listProperties(predicate); while(it.hasNext()) { results.add(it.next().getObject()); } } } /** * Renders a given path into a given StringBuffer, using the prefixes supplied by the * Path's Model. * @param sb the StringBuffer to write into * @param path the path resource */ public static void appendPath(StringBuffer sb, Resource path) { if(path.isURIResource()) { sb.append(FmtUtils.stringForNode(path.asNode(), path.getModel())); } else { appendPathBlankNode(sb, path, SEQUENCE_PATH_SEPARATOR); } } private static void appendNestedPath(StringBuffer sb, Resource path, String separator) { if(path.isURIResource()) { sb.append(FmtUtils.stringForNode(path.asNode(), path.getModel())); } else { sb.append("("); appendPathBlankNode(sb, path, separator); sb.append(")"); } } private static void appendPathBlankNode(StringBuffer sb, Resource path, String separator) { if(path.hasProperty(RDF.first)) { Iterator<RDFNode> it = path.as(RDFList.class).iterator(); while(it.hasNext()) { Resource item = (Resource) it.next(); appendNestedPath(sb, item, SEQUENCE_PATH_SEPARATOR); if(it.hasNext()) { sb.append(" "); sb.append(separator); sb.append(" "); } } } else if(path.hasProperty(SH.inversePath)) { sb.append("^"); if(path.getProperty(SH.inversePath).getObject().isAnon()) { sb.append("("); appendPath(sb, path.getPropertyResourceValue(SH.inversePath)); sb.append(")"); } else { appendPath(sb, path.getPropertyResourceValue(SH.inversePath)); } } else if(path.hasProperty(SH.alternativePath)) { appendNestedPath(sb, path.getPropertyResourceValue(SH.alternativePath), ALTERNATIVE_PATH_SEPARATOR); } else if(path.hasProperty(SH.zeroOrMorePath)) { appendNestedPath(sb, path.getPropertyResourceValue(SH.zeroOrMorePath), SEQUENCE_PATH_SEPARATOR); sb.append("*"); } else if(path.hasProperty(SH.oneOrMorePath)) { appendNestedPath(sb, path.getPropertyResourceValue(SH.oneOrMorePath), SEQUENCE_PATH_SEPARATOR); sb.append("+"); } else if(path.hasProperty(SH.zeroOrOnePath)) { appendNestedPath(sb, path.getPropertyResourceValue(SH.zeroOrOnePath), SEQUENCE_PATH_SEPARATOR); sb.append("?"); } } public static Resource clonePath(Resource path, Model targetModel) { if(path.isURIResource()) { return path.inModel(targetModel); } else { Resource clone = targetModel.createResource(); for(Statement s : path.listProperties().toList()) { if(s.getSubject().hasProperty(RDF.first)) { if(RDF.first.equals(s.getPredicate()) || RDF.rest.equals(s.getPredicate())) { Resource newObject = clonePath(s.getResource(), targetModel); clone.addProperty(s.getPredicate(), newObject); } } else { Resource newObject = clonePath(s.getResource(), targetModel); clone.addProperty(s.getPredicate(), newObject); } } return clone; } } /** * Creates SHACL RDF triples for a given Jena Path (which may have been created using getJenaPath). * @param path the Jena Path * @param model the Model to create the triples in * @return the (root) Resource of the SHACL path */ public static Resource createPath(Path path, Model model) { if(path instanceof P_Alt) { Resource result = model.createResource(); RDFList list = model.createList(Arrays.asList(new RDFNode[] { createPath(((P_Alt) path).getLeft(), model), createPath(((P_Alt) path).getRight(), model) }).iterator()); result.addProperty(SH.alternativePath, list); return result; } else if(path instanceof P_Inverse) { Resource result = model.createResource(); result.addProperty(SH.inversePath, createPath(((P_Inverse) path).getSubPath(), model)); return result; } else if(path instanceof P_Link) { return (Resource) model.asRDFNode(((P_Link) path).getNode()); } else if(path instanceof P_OneOrMore1 || path instanceof P_OneOrMoreN) { Resource result = model.createResource(); result.addProperty(SH.oneOrMorePath, createPath(((P_Path1)path).getSubPath(), model)); return result; } if(path instanceof P_Seq) { return model.createList(Arrays.asList(new RDFNode[] { createPath(((P_Seq) path).getLeft(), model), createPath(((P_Seq) path).getRight(), model) }).iterator()); } else if(path instanceof P_ZeroOrMore1 || path instanceof P_ZeroOrMoreN) { Resource result = model.createResource(); result.addProperty(SH.zeroOrMorePath, createPath(((P_Path1)path).getSubPath(), model)); return result; } else if(path instanceof P_ZeroOrOne) { Resource result = model.createResource(); result.addProperty(SH.zeroOrOnePath, createPath(((P_Path1)path).getSubPath(), model)); return result; } else { throw new IllegalArgumentException("Path element not supported by SHACL syntax: " + path); } } public static Object getJenaPath(Resource path) throws QueryParseException { if(path.isURIResource()) { return path; } else { String pathString = SHACLPaths.getPathString(path); return SHACLPaths.getJenaPath(pathString, path.getModel()); } } /** * Attempts to parse a given string into a Jena Path. * Throws an Exception if the string cannot be parsed. * @param string the string to parse * @param model the Model to operate on (for prefixes) * @return a Path or a Resource if this is a URI */ public static Object getJenaPath(String string, Model model) throws QueryParseException { Query query = ARQFactory.get().createQuery(model, "ASK { ?a \n" + string + "\n ?b }"); Element element = query.getQueryPattern(); if(element instanceof ElementGroup) { Element e = ((ElementGroup)element).getElements().get(0); if(e instanceof ElementPathBlock) { Path path = ((ElementPathBlock) e).getPattern().get(0).getPath(); if(path instanceof P_Link && ((P_Link)path).isForward()) { return model.asRDFNode(((P_Link)path).getNode()); } else { return path; } } else if(e instanceof ElementTriplesBlock) { return model.asRDFNode(((ElementTriplesBlock) e).getPattern().get(0).getPredicate()); } } throw new QueryParseException("Not a SPARQL 1.1 Path expression", 2, 1); } public static String getPathString(Resource path) { StringBuffer sb = new StringBuffer(); appendPath(sb, path); return sb.toString(); } }