| /* |
| * 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.sparql.lang; |
| |
| import java.math.BigInteger ; |
| import java.util.HashSet ; |
| import java.util.Set ; |
| |
| import org.apache.jena.atlas.AtlasException ; |
| import org.apache.jena.atlas.lib.EscapeStr ; |
| import org.apache.jena.atlas.logging.Log ; |
| import org.apache.jena.datatypes.RDFDatatype ; |
| import org.apache.jena.datatypes.TypeMapper ; |
| import org.apache.jena.datatypes.xsd.XSDDatatype ; |
| import org.apache.jena.graph.Node ; |
| import org.apache.jena.graph.NodeFactory ; |
| import org.apache.jena.graph.Triple ; |
| import org.apache.jena.iri.IRI ; |
| import org.apache.jena.n3.JenaURIException ; |
| import org.apache.jena.query.ARQ ; |
| import org.apache.jena.query.QueryParseException ; |
| import org.apache.jena.riot.checker.CheckerIRI ; |
| import org.apache.jena.riot.system.ErrorHandler ; |
| import org.apache.jena.riot.system.ErrorHandlerFactory ; |
| import org.apache.jena.riot.system.RiotLib ; |
| import org.apache.jena.sparql.ARQInternalErrorException ; |
| import org.apache.jena.sparql.core.Prologue ; |
| import org.apache.jena.sparql.core.TriplePath ; |
| import org.apache.jena.sparql.core.Var ; |
| import org.apache.jena.sparql.expr.E_Exists ; |
| import org.apache.jena.sparql.expr.E_NotExists ; |
| import org.apache.jena.sparql.expr.Expr ; |
| import org.apache.jena.sparql.graph.NodeConst ; |
| import org.apache.jena.sparql.modify.request.QuadAccSink ; |
| import org.apache.jena.sparql.path.Path ; |
| import org.apache.jena.sparql.syntax.* ; |
| import org.apache.jena.sparql.util.ExprUtils ; |
| import org.apache.jena.sparql.util.LabelToNodeMap ; |
| import org.apache.jena.vocabulary.RDF ; |
| import org.slf4j.Logger ; |
| import org.slf4j.LoggerFactory ; |
| |
| /** Base class parsers, mainly SPARQL related */ |
| public class ParserBase |
| { |
| // NodeConst |
| protected final Node XSD_TRUE = NodeConst.nodeTrue ; |
| protected final Node XSD_FALSE = NodeConst.nodeFalse ; |
| |
| protected final Node nRDFtype = NodeConst.nodeRDFType ; |
| |
| protected final Node nRDFnil = NodeConst.nodeNil ; |
| protected final Node nRDFfirst = NodeConst.nodeFirst ; |
| protected final Node nRDFrest = NodeConst.nodeRest ; |
| |
| protected final Node nRDFsubject = RDF.Nodes.subject ; |
| protected final Node nRDFpredicate = RDF.Nodes.predicate ; |
| protected final Node nRDFobject = RDF.Nodes.object ; |
| |
| // ---- |
| // Graph patterns, true; in templates, false. |
| private boolean bNodesAreVariables = true ; |
| // In DELETE, false. |
| private boolean bNodesAreAllowed = true ; |
| |
| // label => bNode for construct templates patterns |
| @SuppressWarnings("deprecation") |
| final LabelToNodeMap bNodeLabels = LabelToNodeMap.createBNodeMap() ; |
| |
| // label => bNode (as variable) for graph patterns |
| final LabelToNodeMap anonVarLabels = LabelToNodeMap.createVarMap() ; |
| |
| // This is the map used allocate blank node labels during parsing. |
| // 1/ It is different between CONSTRUCT and the query pattern |
| // 2/ Each BasicGraphPattern is a scope for blank node labels so each |
| // BGP causes the map to be cleared at the start of the BGP |
| |
| LabelToNodeMap activeLabelMap = anonVarLabels ; |
| Set<String> previousLabels = new HashSet<>() ; |
| |
| // Aggregates are only allowed in places where grouping can happen. |
| // e.g. SELECT clause but not a FILTER. |
| private boolean allowAggregatesInExpressions = false ; |
| |
| //LabelToNodeMap listLabelMap = new LabelToNodeMap(true, new VarAlloc("L")) ; |
| // ---- |
| |
| public ParserBase() {} |
| |
| protected Prologue prologue ; |
| public void setPrologue(Prologue prologue) { this.prologue = prologue ; } |
| public Prologue getPrologue() { return prologue ; } |
| |
| protected void setInConstructTemplate(boolean b) { |
| setBNodesAreVariables(!b) ; |
| } |
| |
| protected boolean getBNodesAreVariables() { return bNodesAreVariables ; } |
| |
| protected void setBNodesAreVariables(boolean bNodesAreVariables) { |
| this.bNodesAreVariables = bNodesAreVariables ; |
| if ( bNodesAreVariables ) |
| activeLabelMap = anonVarLabels ; |
| else |
| activeLabelMap = bNodeLabels ; |
| } |
| |
| protected boolean getBNodesAreAllowed() { return bNodesAreAllowed ; } |
| |
| protected void setBNodesAreAllowed(boolean bNodesAreAllowed) { |
| this.bNodesAreAllowed = bNodesAreAllowed ; |
| } |
| |
| protected boolean getAllowAggregatesInExpressions() { |
| return allowAggregatesInExpressions ; |
| } |
| |
| protected void setAllowAggregatesInExpressions(boolean allowAggregatesInExpressions) { |
| this.allowAggregatesInExpressions = allowAggregatesInExpressions; |
| } |
| |
| protected Element compressGroupOfOneGroup(ElementGroup elg) { |
| // remove group of one group. |
| if ( elg.size() == 1 ) { |
| Element e1 = elg.get(0) ; |
| if ( e1 instanceof ElementGroup ) |
| return e1 ; |
| } |
| return elg ; |
| } |
| |
| protected Node createLiteralInteger(String lexicalForm) { |
| return NodeFactory.createLiteral(lexicalForm, XSDDatatype.XSDinteger) ; |
| } |
| |
| protected Node createLiteralDouble(String lexicalForm) { |
| return NodeFactory.createLiteral(lexicalForm, XSDDatatype.XSDdouble) ; |
| } |
| |
| protected Node createLiteralDecimal(String lexicalForm) { |
| return NodeFactory.createLiteral(lexicalForm, XSDDatatype.XSDdecimal) ; |
| } |
| |
| protected Node stripSign(Node node) { |
| if ( !node.isLiteral() ) |
| return node ; |
| String lex = node.getLiteralLexicalForm() ; |
| String lang = node.getLiteralLanguage() ; |
| RDFDatatype dt = node.getLiteralDatatype() ; |
| |
| if ( !lex.startsWith("-") && !lex.startsWith("+") ) |
| throw new ARQInternalErrorException("Literal does not start with a sign: " + lex) ; |
| |
| lex = lex.substring(1) ; |
| return NodeFactory.createLiteral(lex, lang, dt) ; |
| } |
| |
| protected Node createLiteral(String lexicalForm, String langTag, String datatypeURI) { |
| Node n = null ; |
| // Can't have type and lang tag in parsing. |
| if ( datatypeURI != null ) { |
| RDFDatatype dType = TypeMapper.getInstance().getSafeTypeByName(datatypeURI) ; |
| n = NodeFactory.createLiteral(lexicalForm, dType) ; |
| } else if ( langTag != null && !langTag.isEmpty() ) |
| n = NodeFactory.createLiteral(lexicalForm, langTag) ; |
| else |
| n = NodeFactory.createLiteral(lexicalForm) ; |
| return n ; |
| } |
| |
| protected long integerValue(String s) { |
| try { |
| if ( s.startsWith("+") ) |
| s = s.substring(1) ; |
| if ( s.startsWith("0x") ) { |
| // Hex |
| s = s.substring(2) ; |
| return Long.parseLong(s, 16) ; |
| } |
| return Long.parseLong(s) ; |
| } |
| catch (NumberFormatException ex) { |
| try { |
| // Possible too large for a long. |
| BigInteger integer = new BigInteger(s) ; |
| throwParseException("Number '" + s + "' is a valid number but can't not be stored in a long") ; |
| } |
| catch (NumberFormatException ex2) {} |
| throw new QueryParseException(ex, -1, -1) ; |
| } |
| } |
| |
| protected double doubleValue(String s) { |
| if ( s.startsWith("+") ) |
| s = s.substring(1) ; |
| double valDouble = Double.parseDouble(s) ; |
| return valDouble ; |
| } |
| |
| /** Remove first and last characters (e.g. ' or "") from a string */ |
| protected static String stripQuotes(String s) { |
| return s.substring(1, s.length() - 1) ; |
| } |
| |
| /** Remove first 3 and last 3 characters (e.g. ''' or """) from a string */ |
| protected static String stripQuotes3(String s) { |
| return s.substring(3, s.length() - 3) ; |
| } |
| |
| /** remove the first n characters from the string */ |
| public static String stripChars(String s, int n) { |
| return s.substring(n, s.length()) ; |
| } |
| |
| protected Var createVariable(String s, int line, int column) { |
| s = s.substring(1) ; // Drop the marker |
| |
| // This is done by the parser input stream nowadays. |
| // s = unescapeCodePoint(s, line, column) ; |
| // Check \ u did not put in any illegals. |
| return Var.alloc(s) ; |
| } |
| |
| // ---- IRIs and Nodes |
| |
| protected String resolveQuotedIRI(String iriStr, int line, int column) { |
| iriStr = stripQuotes(iriStr) ; |
| return resolveIRI(iriStr, line, column) ; |
| } |
| |
| public static final String ParserLoggerName = "SPARQL" ; |
| private static Logger parserLog = LoggerFactory.getLogger(ParserLoggerName) ; |
| private static ErrorHandler errorHandler = ErrorHandlerFactory.errorHandlerStd(parserLog) ; |
| |
| protected String resolveIRI(String iriStr, int line, int column) { |
| if ( isBNodeIRI(iriStr) ) |
| return iriStr ; |
| |
| if ( getPrologue() != null ) { |
| if ( getPrologue().getResolver() != null ) |
| try { |
| // Used to be errors (pre Jena 2.12.0) |
| // .resolve(iriStr) |
| IRI iri = getPrologue().getResolver().resolveSilent(iriStr) ; |
| if ( true ) |
| CheckerIRI.iriViolations(iri, errorHandler, line, column) ; |
| iriStr = iri.toString() ; |
| } |
| catch (JenaURIException ex) { |
| throwParseException(ex.getMessage(), line, column) ; |
| } |
| } |
| return iriStr ; |
| } |
| |
| protected String resolvePName(String prefixedName, int line, int column) { |
| // It's legal. |
| int idx = prefixedName.indexOf(':') ; |
| |
| // -- Escapes in local name |
| String prefix = prefixedName.substring(0, idx) ; |
| String local = prefixedName.substring(idx + 1) ; |
| local = unescapePName(local, line, column) ; |
| prefixedName = prefix + ":" + local ; |
| // -- |
| |
| String s = getPrologue().expandPrefixedName(prefixedName) ; |
| if ( s == null ) { |
| if ( ARQ.isTrue(ARQ.fixupUndefinedPrefixes) ) |
| return RiotLib.fixupPrefixes.apply(prefixedName) ; |
| throwParseException("Unresolved prefixed name: " + prefixedName, line, column) ; |
| } |
| return s ; |
| } |
| |
| private boolean skolomizedBNodes = ARQ.isTrue(ARQ.constantBNodeLabels) ; |
| |
| protected Node createNode(String iri) { |
| if ( skolomizedBNodes ) |
| return RiotLib.createIRIorBNode(iri) ; |
| else |
| return NodeFactory.createURI(iri) ; |
| } |
| |
| protected boolean isBNodeIRI(String iri) { |
| return skolomizedBNodes && RiotLib.isBNodeIRI(iri) ; |
| } |
| |
| // -------- Basic Graph Patterns and Blank Node label scopes |
| |
| // A BasicGraphPattern is any sequence of TripleBlocks, separated by filters, |
| // but not by other graph patterns. |
| |
| protected void startBasicGraphPattern() |
| { activeLabelMap.clear() ; } |
| |
| protected void endBasicGraphPattern() |
| { previousLabels.addAll(activeLabelMap.getLabels()) ; } |
| |
| protected void startTriplesBlock() |
| { } |
| |
| protected void endTriplesBlock() |
| { } |
| |
| // On entry to a new group, the current BGP is ended. |
| protected void startGroup(ElementGroup elg) { |
| endBasicGraphPattern() ; |
| startBasicGraphPattern() ; |
| } |
| |
| protected void endGroup(ElementGroup elg) { |
| endBasicGraphPattern() ; |
| } |
| |
| // -------- |
| |
| protected void checkConcrete(Node n, int line, int column) { |
| if ( ! n.isConcrete() ) |
| throwParseException("Term is not concrete: "+n, line, column) ; |
| } |
| |
| // BNode from a list |
| // protected Node createListNode() |
| // { return listLabelMap.allocNode() ; } |
| |
| protected Node createListNode(int line, int column) { return createBNode(line, column) ; } |
| |
| // Unlabelled bNode. |
| protected Node createBNode(int line, int column) { |
| if ( !bNodesAreAllowed ) |
| throwParseException("Blank nodes not allowed in DELETE templates", line, column) ; |
| return activeLabelMap.allocNode() ; |
| } |
| |
| // Labelled bNode. |
| protected Node createBNode(String label, int line, int column) { |
| if ( !bNodesAreAllowed ) |
| throwParseException("Blank nodes not allowed in DELETE templates: " + label, line, column) ; |
| if ( previousLabels.contains(label) ) |
| throwParseException("Blank node label reuse not allowed at this point: " + label, line, column) ; |
| |
| // label = unescapeCodePoint(label, line, column) ; |
| return activeLabelMap.asNode(label) ; |
| } |
| |
| protected Node createTripleTerm(Node s, Node p, Node o, int line, int column) { |
| return NodeFactory.createTripleNode(s, p, o); |
| } |
| |
| protected Expr createExprExists(Element element) { |
| return new E_Exists(element) ; |
| } |
| |
| protected Expr createExprNotExists(Element element) { |
| // Could negate here. |
| return new E_NotExists(element) ; |
| } |
| |
| protected String fixupPrefix(String prefix, int line, int column) { |
| // \ u processing! |
| if ( prefix.endsWith(":") ) |
| prefix = prefix.substring(0, prefix.length() - 1) ; |
| return prefix ; |
| } |
| |
| protected void setAccGraph(QuadAccSink acc, Node gn) { |
| acc.setGraph(gn) ; |
| } |
| |
| protected void insert(TripleCollector acc, Node s, Node p, Node o) { |
| acc.addTriple(new Triple(s, p, o)) ; |
| } |
| |
| protected void insert(TripleCollectorMark acc, int index, Node s, Node p, Node o) { |
| acc.addTriple(index, new Triple(s, p, o)) ; |
| } |
| |
| protected void insert(TripleCollector acc, Node s, Node p, Path path, Node o) { |
| if ( p == null ) |
| acc.addTriplePath(new TriplePath(s, path, o)) ; |
| else |
| acc.addTriple(new Triple(s, p, o)) ; |
| } |
| |
| protected void insert(TripleCollectorMark acc, int index, Node s, Node p, Path path, Node o) { |
| if ( p == null ) |
| acc.addTriplePath(index, new TriplePath(s, path, o)) ; |
| else |
| acc.addTriple(index, new Triple(s, p, o)) ; |
| } |
| |
| protected void insert(TripleCollector target, ElementPathBlock source) { |
| for ( TriplePath path : source.getPattern() ) { |
| if ( path.isTriple() ) { |
| target.addTriple(path.asTriple()) ; |
| } else { |
| target.addTriplePath(path) ; |
| } |
| } |
| } |
| |
| protected Expr asExpr(Node n) { |
| return ExprUtils.nodeToExpr(n) ; |
| } |
| |
| protected Expr asExprNoSign(Node n) { |
| String lex = n.getLiteralLexicalForm() ; |
| String lang = n.getLiteralLanguage() ; |
| String dtURI = n.getLiteralDatatypeURI() ; |
| n = createLiteral(lex, lang, dtURI) ; |
| return ExprUtils.nodeToExpr(n) ; |
| } |
| |
| // Utilities to remove escapes in strings. |
| |
| public static String unescapeStr(String s) |
| { return unescape(s, '\\', false, 1, 1) ; } |
| |
| // public static String unescapeCodePoint(String s) |
| // { return unescape(s, '\\', true, 1, 1) ; } |
| // |
| // protected String unescapeCodePoint(String s, int line, int column) |
| // { return unescape(s, '\\', true, line, column) ; } |
| |
| |
| // Do we nee dthe line/column versions? |
| // Why not catch exceptions and comvert to QueryParseException |
| |
| public static String unescapeStr(String s, int line, int column) |
| { return unescape(s, '\\', false, line, column) ; } |
| |
| // Worker function |
| public static String unescape(String s, char escape, boolean pointCodeOnly, int line, int column) { |
| try { |
| return EscapeStr.unescape(s, escape, pointCodeOnly) ; |
| } catch (AtlasException ex) { |
| throwParseException(ex.getMessage(), line, column) ; |
| return null ; |
| } |
| } |
| |
| public static String unescapePName(String s, int line, int column) { |
| char escape = '\\' ; |
| int idx = s.indexOf(escape) ; |
| |
| if ( idx == -1 ) |
| return s ; |
| |
| int len = s.length() ; |
| StringBuilder sb = new StringBuilder() ; |
| |
| for ( int i = 0 ; i < len ; i++ ) { |
| char ch = s.charAt(i) ; |
| // Keep line and column numbers. |
| switch (ch) { |
| case '\n' : |
| case '\r' : |
| line++ ; |
| column = 1 ; |
| break ; |
| default : |
| column++ ; |
| break ; |
| } |
| |
| if ( ch != escape ) { |
| sb.append(ch) ; |
| continue ; |
| } |
| |
| // Escape |
| if ( i >= s.length() - 1 ) |
| throwParseException("Illegal escape at end of string", line, column) ; |
| char ch2 = s.charAt(i + 1) ; |
| column = column + 1 ; |
| i = i + 1 ; |
| |
| switch (ch2) { // PN_LOCAL_ESC |
| case '_' : |
| case '~' : |
| case '.' : |
| case '-' : |
| case '!' : |
| case '$' : |
| case '&' : |
| case '\'' : |
| case '(' : |
| case ')' : |
| case '*' : |
| case '+' : |
| case ',' : |
| case ';' : |
| case '=' : |
| case ':' : |
| case '/' : |
| case '?' : |
| case '#' : |
| case '@' : |
| case '%' : |
| sb.append(ch2) ; |
| break ; |
| default : |
| throwParseException("Illegal prefix name escape: " + ch2, line, column) ; |
| } |
| } |
| return sb.toString() ; |
| } |
| |
| protected void warnDeprecation(String msg) { |
| Log.warn(this, msg) ; |
| } |
| |
| public static void throwParseException(String msg, int line, int column) { |
| throw new QueryParseException("Line " + line + ", column " + column + ": " + msg, line, column) ; |
| } |
| |
| public static void throwParseException(String msg) { |
| throw new QueryParseException(msg, -1, -1) ; |
| } |
| } |