| /** |
| * 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.query; |
| |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.regex.MatchResult; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import org.apache.jena.atlas.lib.Pair; |
| import org.apache.jena.datatypes.RDFDatatype; |
| import org.apache.jena.graph.Node; |
| import org.apache.jena.graph.NodeFactory; |
| import org.apache.jena.iri.IRI; |
| import org.apache.jena.rdf.model.Literal; |
| 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.shared.PrefixMapping; |
| import org.apache.jena.shared.impl.PrefixMappingImpl; |
| import org.apache.jena.sparql.ARQException; |
| import org.apache.jena.sparql.serializer.SerializationContext; |
| import org.apache.jena.sparql.util.FmtUtils; |
| import org.apache.jena.sparql.util.NodeFactoryExtra; |
| import org.apache.jena.update.UpdateFactory; |
| import org.apache.jena.update.UpdateRequest; |
| |
| /** |
| * <p> |
| * A Parameterized SPARQL String is a SPARQL query/update into which values may |
| * be injected. |
| * </p> |
| * <h3>Injecting Values</h3> |
| * <p> |
| * Values may be injected in several ways: |
| * </p> |
| * <ul> |
| * <li>By treating a variable in the SPARQL string as a parameter</li> |
| * <li>Using JDBC style positional parameters</li> |
| * <li>Appending values directly to the command text being built</li> |
| * </ul> |
| * <h4>Variable Parameters</h3> |
| * <p> |
| * Any variable in the command may have a value injected to it, injecting a |
| * value replaces all usages of that variable in the command i.e. substitutes |
| * the variable for a constant, injection is done by textual substitution. |
| * </p> |
| * <h4>Positional Parameters</h4> |
| * <p> |
| * You can use JDBC style positional parameters if you prefer, a JDBC style |
| * parameter is a single {@code ?} followed by whitespace or certain punctuation |
| * characters (currently {@code ; , .}). Positional parameters have a unique |
| * index which reflects the order in which they appear in the string. Positional |
| * parameters use a zero based index. |
| * </p> |
| * <h4>Buffer Usage</h3> |
| * </p> |
| * Additionally you may use this purely as a {@link StringBuffer} replacement |
| * for creating queries since it provides a large variety of convenience methods |
| * for appending things either as-is or as nodes (which causes appropriate |
| * formatting to be applied). |
| * </p> |
| * <h3>Intended Usage</h3> |
| * <p> |
| * The intended usage of this is where using a {@link QuerySolutionMap} as |
| * initial bindings is either inappropriate or not possible e.g. |
| * </p> |
| * <ul> |
| * <li>Generating query/update strings in code without lots of error prone and |
| * messy string concatenation</li> |
| * <li>Preparing a query/update for remote execution</li> |
| * <li>Where you do not want to simply say some variable should have a certain |
| * value but rather wish to insert constants into the query/update in place of |
| * variables</li> |
| * <li>Defending against SPARQL injection when creating a query/update using |
| * some external input, see SPARQL Injection notes for limitations.</li> |
| * <li>Provide a more convenient way to prepend common prefixes to your |
| * query</li> |
| * </ul> |
| * <p> |
| * This class is useful for preparing both queries and updates hence the generic |
| * name as it provides programmatic ways to replace variables in the query with |
| * constants and to add prefix and base declarations. A {@link Query} or |
| * {@link UpdateRequest} can be created using the {@link #asQuery()} and |
| * {@link #asUpdate()} methods assuming the command an instance represents is |
| * actually valid as a query/update. |
| * </p> |
| * <h3>Warnings</h3> |
| * <ol> |
| * <li>Note that this class does not in any way check that your command is |
| * syntactically correct until such time as you try and parse it as a |
| * {@link Query} or {@link UpdateRequest}.</li> |
| * <li>Also note that injection is done purely based on textual replacement, it |
| * does not understand or respect variable scope in any way. For example if your |
| * command text contains sub queries you should ensure that variables within the |
| * sub query which you don't want replaced have distinct names from those in the |
| * outer query you do want replaced (or vice versa)</li> |
| * </ol> |
| * <h3>SPARQL Injection Notes</h3> |
| * <p> |
| * While this class was in part designed to prevent SPARQL injection it is by no |
| * means foolproof because it works purely at the textual level. The current |
| * version of the code addresses some possible attack vectors that the |
| * developers have identified but we do not claim to be sufficiently devious to |
| * have thought of and prevented every possible attack vector. |
| * </p> |
| * <p> |
| * Therefore we <strong>strongly</strong> recommend that users concerned about |
| * SPARQL Injection attacks perform their own validation on provided parameters |
| * and test their use of this class themselves prior to its use in any security |
| * conscious deployment. We also recommend that users do not use easily |
| * guess-able variable names for their parameters as these can allow a chained |
| * injection attack though generally speaking the code should prevent these. |
| * </p> |
| */ |
| public class ParameterizedSparqlString implements PrefixMapping { |
| |
| private Model model = ModelFactory.createDefaultModel(); |
| |
| private StringBuilder cmd = new StringBuilder(); |
| private String baseUri; |
| private Map<String, Node> params = new HashMap<>(); |
| private Map<Integer, Node> positionalParams = new HashMap<>(); |
| private PrefixMapping prefixes; |
| private Map<String, ValueReplacement> valuesReplacements = new HashMap<>(); |
| private Syntax syntax = Syntax.defaultQuerySyntax; |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param map |
| * Initial Parameters to inject |
| * @param base |
| * Base URI |
| * @param prefixes |
| * Prefix Mapping |
| */ |
| public ParameterizedSparqlString(String command, QuerySolutionMap map, String base, PrefixMapping prefixes) { |
| if (command != null) |
| this.cmd.append(command); |
| this.setParams(map); |
| this.baseUri = (base != null && !base.equals("") ? base : null); |
| this.prefixes = new PrefixMappingImpl(); |
| if (prefixes != null) |
| this.prefixes.setNsPrefixes(prefixes); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param map |
| * Initial Parameters to inject |
| * @param base |
| * Base URI |
| */ |
| public ParameterizedSparqlString(String command, QuerySolutionMap map, String base) { |
| this(command, map, base, null); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param map |
| * Initial Parameters to inject |
| * @param prefixes |
| * Prefix Mapping |
| */ |
| public ParameterizedSparqlString(String command, QuerySolutionMap map, PrefixMapping prefixes) { |
| this(command, map, null, prefixes); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param map |
| * Initial Parameters to inject |
| */ |
| public ParameterizedSparqlString(String command, QuerySolutionMap map) { |
| this(command, map, null, null); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param base |
| * Base URI |
| * @param prefixes |
| * Prefix Mapping |
| */ |
| public ParameterizedSparqlString(String command, String base, PrefixMapping prefixes) { |
| this(command, null, base, prefixes); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param prefixes |
| * Prefix Mapping |
| */ |
| public ParameterizedSparqlString(String command, PrefixMapping prefixes) { |
| this(command, null, null, prefixes); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| * @param base |
| * Base URI |
| */ |
| public ParameterizedSparqlString(String command, String base) { |
| this(command, null, base, null); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param command |
| * Raw Command Text |
| */ |
| public ParameterizedSparqlString(String command) { |
| this(command, null, null, null); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param map |
| * Initial Parameters to inject |
| * @param prefixes |
| * Prefix Mapping |
| */ |
| public ParameterizedSparqlString(QuerySolutionMap map, PrefixMapping prefixes) { |
| this(null, map, null, prefixes); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param map |
| * Initial Parameters to inject |
| */ |
| public ParameterizedSparqlString(QuerySolutionMap map) { |
| this(null, map, null, null); |
| } |
| |
| /** |
| * Creates a new parameterized string |
| * |
| * @param prefixes |
| * Prefix Mapping |
| */ |
| public ParameterizedSparqlString(PrefixMapping prefixes) { |
| this(null, null, null, prefixes); |
| } |
| |
| /** |
| * Creates a new parameterized string with an empty command text |
| */ |
| public ParameterizedSparqlString() { |
| this("", null, null, null); |
| } |
| |
| /** |
| * Gets the syntax used for parsing when calling {@link #asQuery()} or |
| * {@link #asUpdate()} |
| * |
| * |
| * @return Syntax |
| */ |
| public Syntax getSyntax() { |
| return this.syntax; |
| } |
| |
| /** |
| * Sets the syntax used for parsing when calling {@link #asQuery()} or |
| * {@link #asUpdate()} |
| * |
| * @param syntax |
| * Syntax |
| */ |
| public void setSyntax(Syntax syntax) { |
| if (syntax == null) |
| return; |
| |
| this.syntax = syntax; |
| } |
| |
| /** |
| * Sets the command text, overwriting any existing command text. If you want |
| * to append to the command text use one of the {@link #append(String)}, |
| * {@link #appendIri(String)}, {@link #appendLiteral(String)} or |
| * {@link #appendNode(Node)} methods instead |
| * |
| * @param command |
| * Command Text |
| */ |
| public void setCommandText(String command) { |
| this.cmd = new StringBuilder(); |
| this.cmd.append(command); |
| } |
| |
| /** |
| * Appends some text as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using the |
| * {@link #appendLiteral(String)} or {@link #appendIri(String)} method as |
| * appropriate |
| * |
| * @param text |
| * Text to append |
| */ |
| public void append(String text) { |
| this.cmd.append(text); |
| } |
| |
| /** |
| * Appends a character as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using one of the |
| * {@code appendLiteral()} methods |
| * |
| * @param c |
| * Character to append |
| */ |
| public void append(char c) { |
| this.cmd.append(c); |
| } |
| |
| /** |
| * Appends a boolean as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using the |
| * {@link #appendLiteral(boolean)} method |
| * |
| * @param b |
| * Boolean to append |
| */ |
| public void append(boolean b) { |
| this.cmd.append(b); |
| } |
| |
| /** |
| * Appends a double as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using the |
| * {@link #appendLiteral(double)} method |
| * |
| * @param d |
| * Double to append |
| */ |
| public void append(double d) { |
| this.cmd.append(d); |
| } |
| |
| /** |
| * Appends a float as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using the |
| * {@link #appendLiteral(float)} method |
| * |
| * @param f |
| * Float to append |
| */ |
| public void append(float f) { |
| this.cmd.append(f); |
| } |
| |
| /** |
| * Appends an integer as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using the |
| * {@link #appendLiteral(int)} method |
| * |
| * @param i |
| * Integer to append |
| */ |
| public void append(int i) { |
| this.cmd.append(i); |
| } |
| |
| /** |
| * Appends a long as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider using the |
| * {@link #appendLiteral(long)} method |
| * |
| * @param l |
| * Long to append |
| */ |
| public void append(long l) { |
| this.cmd.append(l); |
| } |
| |
| /** |
| * Appends an object as-is to the existing command text, to ensure correct |
| * formatting when used as a constant consider converting into a more |
| * specific type and using the appropriate {@code appendLiteral()}, |
| * {@code appendIri()} or {@code appendNode} methods |
| * |
| * @param obj |
| * Object to append |
| */ |
| public void append(Object obj) { |
| this.cmd.append(obj); |
| } |
| |
| /** |
| * Appends a Node to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param n |
| * Node to append |
| */ |
| public void appendNode(Node n) { |
| SerializationContext context = new SerializationContext(this.prefixes); |
| context.setBaseIRI(this.baseUri); |
| this.cmd.append(this.stringForNode(n, context)); |
| } |
| |
| /** |
| * Appends a Node to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param n |
| * Node to append |
| */ |
| public void appendNode(RDFNode n) { |
| this.appendNode(n.asNode()); |
| } |
| |
| /** |
| * Appends a URI to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param uri |
| * URI to append |
| */ |
| public void appendIri(String uri) { |
| this.appendNode(NodeFactory.createURI(uri)); |
| } |
| |
| /** |
| * Appends an IRI to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param iri |
| * IRI to append |
| */ |
| public void appendIri(IRI iri) { |
| this.appendNode(NodeFactory.createURI(iri.toString())); |
| } |
| |
| /** |
| * Appends a simple literal as a constant using appropriate formatting |
| * |
| * @param value |
| * Lexical Value |
| */ |
| public void appendLiteral(String value) { |
| this.appendNode(NodeFactoryExtra.createLiteralNode(value, null, null)); |
| } |
| |
| /** |
| * Appends a literal with a lexical value and language to the command text |
| * as a constant using appropriate formatting |
| * |
| * @param value |
| * Lexical Value |
| * @param lang |
| * Language |
| */ |
| public void appendLiteral(String value, String lang) { |
| this.appendNode(NodeFactoryExtra.createLiteralNode(value, lang, null)); |
| } |
| |
| /** |
| * Appends a Typed Literal to the command text as a constant using |
| * appropriate formatting |
| * |
| * @param value |
| * Lexical Value |
| * @param datatype |
| * Datatype |
| */ |
| public void appendLiteral(String value, RDFDatatype datatype) { |
| this.appendNode(NodeFactoryExtra.createLiteralNode(value, null, datatype.getURI())); |
| } |
| |
| /** |
| * Appends a boolean to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param b |
| * Boolean to append |
| */ |
| public void appendLiteral(boolean b) { |
| this.appendNode(this.model.createTypedLiteral(b)); |
| } |
| |
| /** |
| * Appends an integer to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param i |
| * Integer to append |
| */ |
| public void appendLiteral(int i) { |
| this.appendNode(NodeFactoryExtra.intToNode(i)); |
| } |
| |
| /** |
| * Appends a long to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param l |
| * Long to append |
| */ |
| public void appendLiteral(long l) { |
| this.appendNode(NodeFactoryExtra.intToNode(l)); |
| } |
| |
| /** |
| * Appends a float to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param f |
| * Float to append |
| */ |
| public void appendLiteral(float f) { |
| this.appendNode(this.model.createTypedLiteral(f)); |
| } |
| |
| /** |
| * Appends a double to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param d |
| * Double to append |
| */ |
| public void appendLiteral(double d) { |
| this.appendNode(this.model.createTypedLiteral(d)); |
| } |
| |
| /** |
| * Appends a date time to the command text as a constant using appropriate |
| * formatting |
| * |
| * @param dt |
| * Date Time to append |
| */ |
| public void appendLiteral(Calendar dt) { |
| this.appendNode(this.model.createTypedLiteral(dt)); |
| } |
| |
| /** |
| * Gets the basic Command Text |
| * <p> |
| * <strong>Note:</strong> This will not reflect any injected parameters, to |
| * see the command with injected parameters invoke the {@link #toString()} |
| * method |
| * </p> |
| * |
| * @return Command Text |
| */ |
| public String getCommandText() { |
| return this.cmd.toString(); |
| } |
| |
| /** |
| * Sets the Base URI which will be prepended to the query/update |
| * |
| * @param base |
| * Base URI |
| */ |
| public void setBaseUri(String base) { |
| this.baseUri = base; |
| } |
| |
| /** |
| * Gets the Base URI which will be prepended to a query |
| * |
| * @return Base URI |
| */ |
| public String getBaseUri() { |
| return this.baseUri; |
| } |
| |
| /** |
| * Helper method which does the validation of the parameters |
| * |
| * @param n |
| * Node |
| */ |
| protected void validateParameterValue(Node n) { |
| if (n.isURI()) { |
| if (n.getURI().contains(">")) |
| throw new ARQException("Value for the parameter contains a SPARQL injection risk"); |
| } |
| } |
| |
| /** |
| * Sets the Parameters |
| * |
| * @param map |
| * Parameters |
| */ |
| public void setParams(QuerySolutionMap map) { |
| if (map != null) { |
| Iterator<String> iter = map.varNames(); |
| while (iter.hasNext()) { |
| String var = iter.next(); |
| this.setParam(var, map.get(var).asNode()); |
| } |
| } |
| } |
| |
| /** |
| * Sets a Positional Parameter |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given variable |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param n |
| * Node |
| */ |
| public void setParam(int index, Node n) { |
| if (index < 0) |
| throw new IndexOutOfBoundsException(); |
| if (n != null) { |
| this.validateParameterValue(n); |
| this.positionalParams.put(index, n); |
| } else { |
| this.positionalParams.remove(index); |
| } |
| } |
| |
| /** |
| * Sets a variable parameter |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param n |
| * Value |
| * |
| */ |
| public void setParam(String var, Node n) { |
| if (var == null) |
| throw new IllegalArgumentException("var cannot be null"); |
| if (var.startsWith("?") || var.startsWith("$")) |
| var = var.substring(1); |
| if (n != null) { |
| this.validateParameterValue(n); |
| this.params.put(var, n); |
| } else { |
| this.params.remove(var); |
| } |
| } |
| |
| /** |
| * Sets a positional parameter |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param n |
| * Node |
| */ |
| public void setParam(int index, RDFNode n) { |
| this.setParam(index, n.asNode()); |
| } |
| |
| /** |
| * Sets a variable parameter |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param n |
| * Value |
| */ |
| public void setParam(String var, RDFNode n) { |
| this.setParam(var, n.asNode()); |
| } |
| |
| /** |
| * Sets a positional parameter to an IRI |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param iri |
| * IRI |
| */ |
| public void setIri(int index, String iri) { |
| this.setParam(index, NodeFactory.createURI(iri)); |
| } |
| |
| /** |
| * Sets a variable parameter to an IRI |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param iri |
| * IRI |
| */ |
| public void setIri(String var, String iri) { |
| this.setParam(var, NodeFactory.createURI(iri)); |
| } |
| |
| /** |
| * Sets a positional parameter to an IRI |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param iri |
| * IRI |
| */ |
| public void setIri(int index, IRI iri) { |
| this.setIri(index, iri.toString()); |
| } |
| |
| /** |
| * Sets a variable parameter to an IRI |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param iri |
| * IRI |
| */ |
| public void setIri(String var, IRI iri) { |
| this.setIri(var, iri.toString()); |
| } |
| |
| /** |
| * Sets a positional parameter to an IRI |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param url |
| * URL |
| */ |
| public void setIri(int index, URL url) { |
| this.setIri(index, url.toString()); |
| } |
| |
| /** |
| * Sets a variable parameter to an IRI |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param url |
| * URL used as IRI |
| * |
| */ |
| public void setIri(String var, URL url) { |
| this.setIri(var, url.toString()); |
| } |
| |
| /** |
| * Sets a positional parameter to a Literal |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param lit |
| * Value |
| * |
| */ |
| public void setLiteral(int index, Literal lit) { |
| this.setParam(index, lit.asNode()); |
| } |
| |
| /** |
| * Sets a variable parameter to a Literal |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param lit |
| * Value |
| * |
| */ |
| public void setLiteral(String var, Literal lit) { |
| this.setParam(var, lit.asNode()); |
| } |
| |
| /** |
| * Sets a positional parameter to a literal |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param value |
| * Lexical Value |
| * |
| */ |
| public void setLiteral(int index, String value) { |
| this.setParam(index, NodeFactoryExtra.createLiteralNode(value, null, null)); |
| } |
| |
| /** |
| * Sets a variable parameter to a literal |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param value |
| * Lexical Value |
| * |
| */ |
| public void setLiteral(String var, String value) { |
| this.setParam(var, NodeFactoryExtra.createLiteralNode(value, null, null)); |
| } |
| |
| /** |
| * Sets a positional parameter to a literal with a language |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional index |
| * @param value |
| * Lexical Value |
| * @param lang |
| * Language |
| * |
| */ |
| public void setLiteral(int index, String value, String lang) { |
| this.setParam(index, NodeFactoryExtra.createLiteralNode(value, lang, null)); |
| } |
| |
| /** |
| * Sets a variable parameter to a literal with a language |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param value |
| * Lexical Value |
| * @param lang |
| * Language |
| * |
| */ |
| public void setLiteral(String var, String value, String lang) { |
| this.setParam(var, NodeFactoryExtra.createLiteralNode(value, lang, null)); |
| } |
| |
| /** |
| * Sets a positional parameter to a typed literal |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(int)} for the given index |
| * </p> |
| * |
| * @param index |
| * Positional Index |
| * @param value |
| * Lexical Value |
| * @param datatype |
| * Datatype |
| * |
| */ |
| public void setLiteral(int index, String value, RDFDatatype datatype) { |
| this.setParam(index, this.model.createTypedLiteral(value, datatype)); |
| } |
| |
| /** |
| * Sets a variable parameter to a typed literal |
| * <p> |
| * Setting a parameter to null is equivalent to calling |
| * {@link #clearParam(String)} for the given variable |
| * </p> |
| * |
| * @param var |
| * Variable |
| * @param value |
| * Lexical Value |
| * @param datatype |
| * Datatype |
| * |
| */ |
| public void setLiteral(String var, String value, RDFDatatype datatype) { |
| this.setParam(var, this.model.createTypedLiteral(value, datatype)); |
| } |
| |
| /** |
| * Sets a positional parameter to a boolean literal |
| * |
| * @param index |
| * Positional Index |
| * @param value |
| * boolean |
| */ |
| public void setLiteral(int index, boolean value) { |
| this.setParam(index, this.model.createTypedLiteral(value)); |
| } |
| |
| /** |
| * Sets a variable parameter to a boolean literal |
| * |
| * @param var |
| * Variable |
| * @param value |
| * boolean |
| */ |
| public void setLiteral(String var, boolean value) { |
| this.setParam(var, this.model.createTypedLiteral(value)); |
| } |
| |
| /** |
| * Sets a positional parameter to an integer literal |
| * |
| * @param index |
| * Positional Index |
| * @param i |
| * Integer Value |
| */ |
| public void setLiteral(int index, int i) { |
| this.setParam(index, NodeFactoryExtra.intToNode(i)); |
| } |
| |
| /** |
| * Sets a variable parameter to an integer literal |
| * |
| * @param var |
| * Variable |
| * @param i |
| * Integer Value |
| */ |
| public void setLiteral(String var, int i) { |
| this.setParam(var, NodeFactoryExtra.intToNode(i)); |
| } |
| |
| /** |
| * Sets a positional parameter to an integer literal |
| * |
| * @param index |
| * Positional Index |
| * @param l |
| * Integer Value |
| */ |
| public void setLiteral(int index, long l) { |
| this.setParam(index, NodeFactoryExtra.intToNode(l)); |
| } |
| |
| /** |
| * Sets a variable parameter to an integer literal |
| * |
| * @param var |
| * Variable |
| * @param l |
| * Integer Value |
| */ |
| public void setLiteral(String var, long l) { |
| this.setParam(var, NodeFactoryExtra.intToNode(l)); |
| } |
| |
| /** |
| * Sets a positional parameter to a float literal |
| * |
| * @param index |
| * Positional Index |
| * @param f |
| * Float value |
| */ |
| public void setLiteral(int index, float f) { |
| this.setParam(index, NodeFactoryExtra.floatToNode(f)); |
| } |
| |
| /** |
| * Sets a variable parameter to a float literal |
| * |
| * @param var |
| * Variable |
| * @param f |
| * Float value |
| */ |
| public void setLiteral(String var, float f) { |
| this.setParam(var, NodeFactoryExtra.floatToNode(f)); |
| } |
| |
| /** |
| * Sets a positional parameter to a double literal |
| * |
| * @param index |
| * Positional Index |
| * @param d |
| * Double value |
| */ |
| public void setLiteral(int index, double d) { |
| this.setParam(index, this.model.createTypedLiteral(d)); |
| } |
| |
| /** |
| * Sets a variable parameter to a double literal |
| * |
| * @param var |
| * Variable |
| * @param d |
| * Double value |
| */ |
| public void setLiteral(String var, double d) { |
| this.setParam(var, this.model.createTypedLiteral(d)); |
| } |
| |
| /** |
| * Sets a positional parameter to a date time literal |
| * |
| * @param index |
| * Positional Index |
| * @param dt |
| * Date Time value |
| */ |
| public void setLiteral(int index, Calendar dt) { |
| this.setParam(index, this.model.createTypedLiteral(dt)); |
| } |
| |
| /** |
| * Sets a variable parameter to a date time literal |
| * |
| * @param var |
| * Variable |
| * @param dt |
| * Date Time value |
| */ |
| public void setLiteral(String var, Calendar dt) { |
| this.setParam(var, this.model.createTypedLiteral(dt)); |
| } |
| |
| /** |
| * Gets the current value for a variable parameter |
| * |
| * @param var |
| * Variable |
| * @return Current value or null if not set |
| */ |
| public Node getParam(String var) { |
| return this.params.get(var); |
| } |
| |
| /** |
| * Gets the current value for a positional parameter |
| * |
| * @param index |
| * Positional Index |
| * @return Current value or null if not set |
| */ |
| public Node getParam(int index) { |
| return this.positionalParams.get(index); |
| } |
| |
| /** |
| * Gets the variable names which are currently treated as variable |
| * parameters (i.e. have values set for them) |
| * |
| * @return Iterator of variable names |
| */ |
| @Deprecated |
| public Iterator<String> getVars() { |
| return this.params.keySet().iterator(); |
| } |
| |
| /** |
| * Gets the map of currently set variable parameters, this will be an |
| * unmodifiable map |
| * |
| * @return Map of variable names and values |
| */ |
| public Map<String, Node> getVariableParameters() { |
| return Collections.unmodifiableMap(this.params); |
| } |
| |
| /** |
| * Gets the map of currently set positional parameters, this will be an |
| * unmodifiable map |
| * |
| * @return Map of positional indexes and values |
| */ |
| public Map<Integer, Node> getPositionalParameters() { |
| return Collections.unmodifiableMap(this.positionalParams); |
| } |
| |
| // TODO: Detecting eligible variable parameters |
| // public Iterator<String> getEligibleVariableParameters() { |
| // |
| // } |
| |
| /** |
| * Gets the eligible positional parameters i.e. detected positional |
| * parameters that may be set in the command string as it currently stands |
| * |
| * @return Iterator of eligible positional parameters |
| */ |
| public Iterator<Integer> getEligiblePositionalParameters() { |
| Pattern p = Pattern.compile("(\\?)[\\s;,.]"); |
| List<Integer> positions = new ArrayList<>(); |
| int index = 0; |
| Matcher matcher = p.matcher(this.cmd.toString()); |
| while (matcher.find()) { |
| positions.add(index); |
| index++; |
| } |
| return positions.iterator(); |
| } |
| |
| /** |
| * Clears the value for a variable or values parameter so the given variable |
| * will not * have a value injected |
| * |
| * @param var |
| * Variable |
| */ |
| public void clearParam(String var) { |
| this.params.remove(var); |
| this.valuesReplacements.remove(var); |
| } |
| |
| /** |
| * Clears the value for a positional parameter |
| * |
| * @param index |
| * Positional Index |
| */ |
| public void clearParam(int index) { |
| this.positionalParams.remove(index); |
| } |
| |
| /** |
| * Clears all values for variable, values and positional parameters |
| */ |
| public void clearParams() { |
| this.params.clear(); |
| this.valuesReplacements.clear(); |
| this.positionalParams.clear(); |
| } |
| |
| /** |
| * Helper method which checks whether it is safe to inject to a variable |
| * parameter the given value |
| * |
| * @param command |
| * Current command string |
| * @param var |
| * Variable |
| * @param n |
| * Value to inject |
| * @throws ARQException |
| * Thrown if not safe to inject, error message will describe why |
| * it is unsafe to inject |
| */ |
| protected void validateSafeToInject(String command, String var, Node n) throws ARQException { |
| // Looks for the known injection attack vectors and throws an error if |
| // any are encountered |
| |
| // A ?var surrounded by " or ' where the variable is a literal is an |
| // attack vector |
| Pattern p = Pattern.compile("\"[?$]" + var + "\"|'[?$]" + var + "'"); |
| |
| if (p.matcher(command).find() && n.isLiteral()) { |
| throw new ARQException("Command string is vunerable to injection attack, variable ?" + var |
| + " appears surrounded directly by quotes and is bound to a literal which provides a SPARQL injection attack vector"); |
| } |
| |
| // Parse out delimiter info |
| DelimiterInfo delims = this.findDelimiters(command); |
| |
| // Check each occurrence of the variable for safety |
| p = Pattern.compile("([?$]" + var + ")([^\\w]|$)"); |
| Matcher matcher = p.matcher(command); |
| while (matcher.find()) { |
| MatchResult posMatch = matcher.toMatchResult(); |
| |
| if (n.isLiteral()) { |
| if (delims.isInsideLiteral(posMatch.start(1), posMatch.end(1))) { |
| throw new ARQException("Command string is vunerable to injection attack, variable ?" + var |
| + " appears inside of a literal and is bound to a literal which provides a SPARQL injection attack vector"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Helper method which checks whether it is safe to inject to a positional |
| * parameter the given value |
| * |
| * @param command |
| * Current command string |
| * @param index |
| * Positional parameter index |
| * @param position |
| * Position within the command string at which the positional |
| * parameter occurs |
| * @param n |
| * Value to inject |
| * @throws ARQException |
| * Thrown if not safe to inject, error message will describe why |
| * it is unsafe to inject |
| */ |
| protected void validateSafeToInject(String command, int index, int position, Node n) throws ARQException { |
| // Parse out delimiter info |
| DelimiterInfo delims = this.findDelimiters(command); |
| |
| // Check each occurrence of the variable for safety |
| if (n.isLiteral()) { |
| if (delims.isInsideLiteral(position, position)) { |
| throw new ARQException("Command string is vunerable to injection attack, a positional paramter (index " |
| + index |
| + ") appears inside of a literal and is bound to a literal which provides a SPARQL injection attack vector"); |
| } |
| } |
| } |
| |
| /** |
| * Helper method which does light parsing on the command string to find the |
| * position of all relevant delimiters |
| * |
| * @param command |
| * Command String |
| * @return DelimiterInfo |
| */ |
| protected final DelimiterInfo findDelimiters(String command) { |
| DelimiterInfo delims = new DelimiterInfo(); |
| delims.parseFrom(command); |
| return delims; |
| } |
| |
| protected final String stringForNode(Node n, SerializationContext context) { |
| String str = FmtUtils.stringForNode(n, context); |
| if (n.isLiteral() && str.contains("'")) { |
| // Should escape ' to avoid a possible injection vulnerability |
| str = str.replace("'", "\\'"); |
| } |
| return str; |
| } |
| |
| /** |
| * <p> |
| * This method is where the actual work happens, the original command text |
| * is always preserved and we just generated a temporary command string by |
| * prepending the defined Base URI and namespace prefixes at the start of |
| * the command and injecting the set parameters into a copy of that base |
| * command string and return the resulting command. |
| * </p> |
| * <p> |
| * This class makes no guarantees about the validity of the returned string |
| * for use as a SPARQL Query or Update, for example if a variable parameter |
| * was injected which was mentioned in the SELECT variables list you'd have |
| * a syntax error when you try to parse the query. If you run into issues |
| * like this try using a mixture of variable and positional parameters. |
| * </p> |
| * |
| * @throws ARQException |
| * May be thrown if the code detects a SPARQL Injection |
| * vulnerability because of the interaction of the command |
| * string and the injected variables |
| */ |
| @Override |
| public String toString() { |
| String command = this.cmd.toString(); |
| Pattern p; |
| |
| // Go ahead and inject Variable Parameters |
| SerializationContext context = new SerializationContext(this.prefixes); |
| context.setBaseIRI(this.baseUri); |
| for (String var : this.params.keySet()) { |
| Node n = this.params.get(var); |
| if (n == null) { |
| continue; |
| } |
| this.validateSafeToInject(command, var, n); |
| |
| p = Pattern.compile("([?$]" + var + ")([^\\w]|$)"); |
| command = p.matcher(command).replaceAll(Matcher.quoteReplacement(this.stringForNode(n, context)) + "$2"); |
| } |
| |
| // Inject Values Parameters |
| command = applyValues(command, context); |
| |
| // Then inject Positional Parameters |
| // To do this we need to find the ? we will replace |
| p = Pattern.compile("(\\?)[\\s;,.]"); |
| int index = -1; |
| int adj = 0; |
| Matcher matcher = p.matcher(command); |
| while (matcher.find()) { |
| index++; |
| MatchResult posMatch = matcher.toMatchResult(); |
| |
| Node n = this.positionalParams.get(index); |
| if (n == null) |
| continue; |
| this.validateSafeToInject(command, index, posMatch.start(1) + adj, n); |
| |
| String nodeStr = this.stringForNode(n, context); |
| command = command.substring(0, posMatch.start() + adj) + nodeStr |
| + command.substring(posMatch.start() + adj + 1); |
| // Because we are using a matcher over the string state prior to |
| // starting replacements we need to |
| // track the offset adjustments to make |
| adj += nodeStr.length() - 1; |
| } |
| |
| // Build the final command string |
| StringBuilder finalCmd = new StringBuilder(); |
| |
| // Add BASE declaration |
| if (this.baseUri != null) { |
| finalCmd.append("BASE "); |
| finalCmd.append(FmtUtils.stringForURI(this.baseUri, null, null)); |
| finalCmd.append('\n'); |
| } |
| |
| // Then pre-pend prefixes |
| |
| for (String prefix : this.prefixes.getNsPrefixMap().keySet()) { |
| finalCmd.append("PREFIX "); |
| finalCmd.append(prefix); |
| finalCmd.append(": "); |
| finalCmd.append(FmtUtils.stringForURI(this.prefixes.getNsPrefixURI(prefix), null, null)); |
| finalCmd.append('\n'); |
| } |
| |
| finalCmd.append(command); |
| return finalCmd.toString(); |
| } |
| |
| /** |
| * Attempts to take the command text with parameters injected from the |
| * {@link #toString()} method and parse it as a {@link Query} |
| * |
| * @return Query if the command text is a valid SPARQL query |
| * @exception QueryException |
| * Thrown if the command text does not parse |
| */ |
| public Query asQuery() throws QueryException { |
| return asQuery(this.syntax); |
| } |
| |
| /** |
| * Attempts to take the command text with parameters injected from the |
| * {@link #toString()} method and parse it as a {@link Query} using the |
| * given {@link Syntax} syntax |
| * |
| * @return Query if the command text is a valid SPARQL query |
| * @exception QueryException |
| * Thrown if the command text does not parse |
| */ |
| public Query asQuery(Syntax syntax) { |
| return QueryFactory.create(this.toString(), syntax); |
| } |
| |
| /** |
| * Attempts to take the command text with parameters injected from the |
| * {@link #toString()} method and parse it as a {@link UpdateRequest} |
| * |
| * @return Update if the command text is a valid SPARQL Update request |
| * (one/more update commands) |
| */ |
| public UpdateRequest asUpdate() { |
| return asUpdate(this.syntax); |
| } |
| |
| /** |
| * Attempts to take the command text with parameters injected from the |
| * {@link #toString()} method and parse it as a {@link UpdateRequest} using |
| * the given {@link Syntax} |
| * |
| * @return Update if the command text is a valid SPARQL Update request |
| * (one/more update commands) |
| */ |
| public UpdateRequest asUpdate(Syntax syntax) { |
| return UpdateFactory.create(this.toString(), syntax); |
| } |
| |
| /** |
| * Makes a full copy of this parameterized string |
| * |
| * @return Copy of the string |
| */ |
| public ParameterizedSparqlString copy() { |
| return this.copy(true, true, true); |
| } |
| |
| /** |
| * Makes a copy of the command text, base URI and prefix mapping and |
| * optionally copies parameter values |
| * |
| * @param copyParams |
| * Whether to copy parameters |
| * @return Copy of the string |
| */ |
| public ParameterizedSparqlString copy(boolean copyParams) { |
| return this.copy(copyParams, true, true); |
| } |
| |
| /** |
| * Makes a copy of the command text and optionally copies other aspects |
| * |
| * @param copyParams |
| * Whether to copy parameters |
| * @param copyBase |
| * Whether to copy the Base URI |
| * @param copyPrefixes |
| * Whether to copy the prefix mappings |
| * @return Copy of the string |
| */ |
| public ParameterizedSparqlString copy(boolean copyParams, boolean copyBase, boolean copyPrefixes) { |
| ParameterizedSparqlString copy = new ParameterizedSparqlString(this.cmd.toString(), null, |
| (copyBase ? this.baseUri : null), (copyPrefixes ? this.prefixes : null)); |
| if (copyParams) { |
| Iterator<String> vars = this.getVars(); |
| while (vars.hasNext()) { |
| String var = vars.next(); |
| copy.setParam(var, this.getParam(var)); |
| } |
| for (Entry<Integer, Node> entry : this.positionalParams.entrySet()) { |
| copy.setParam(entry.getKey(), entry.getValue()); |
| } |
| } |
| copy.setSyntax(copy.getSyntax()); |
| return copy; |
| } |
| |
| @Override |
| public PrefixMapping setNsPrefix(String prefix, String uri) { |
| return this.prefixes.setNsPrefix(prefix, uri); |
| } |
| |
| @Override |
| public PrefixMapping removeNsPrefix(String prefix) { |
| return this.prefixes.removeNsPrefix(prefix); |
| } |
| |
| @Override |
| public PrefixMapping clearNsPrefixMap() { |
| return this.prefixes.clearNsPrefixMap(); |
| } |
| |
| @Override |
| public PrefixMapping setNsPrefixes(PrefixMapping other) { |
| return this.prefixes.setNsPrefixes(other); |
| } |
| |
| @Override |
| public PrefixMapping setNsPrefixes(Map<String, String> map) { |
| return this.prefixes.setNsPrefixes(map); |
| } |
| |
| @Override |
| public PrefixMapping withDefaultMappings(PrefixMapping map) { |
| return this.prefixes.withDefaultMappings(map); |
| } |
| |
| @Override |
| public String getNsPrefixURI(String prefix) { |
| return this.prefixes.getNsPrefixURI(prefix); |
| } |
| |
| @Override |
| public String getNsURIPrefix(String uri) { |
| return this.prefixes.getNsURIPrefix(uri); |
| } |
| |
| @Override |
| public Map<String, String> getNsPrefixMap() { |
| return this.prefixes.getNsPrefixMap(); |
| } |
| |
| @Override |
| public String expandPrefix(String prefixed) { |
| return this.prefixes.expandPrefix(prefixed); |
| } |
| |
| @Override |
| public String shortForm(String uri) { |
| return this.prefixes.shortForm(uri); |
| } |
| |
| @Override |
| public String qnameFor(String uri) { |
| return this.prefixes.qnameFor(uri); |
| } |
| |
| @Override |
| public boolean hasNoMappings() { |
| return this.prefixes.hasNoMappings(); |
| } |
| |
| @Override |
| public int numPrefixes() { |
| return this.prefixes.numPrefixes(); |
| } |
| |
| @Override |
| public PrefixMapping lock() { |
| return this.prefixes.lock(); |
| } |
| |
| @Override |
| public boolean samePrefixMappingAs(PrefixMapping other) { |
| return this.prefixes.samePrefixMappingAs(other); |
| } |
| |
| /** |
| * Represents information about delimiters in a string |
| * |
| */ |
| private class DelimiterInfo { |
| private List<Pair<Integer, String>> starts = new ArrayList<>(); |
| private Map<Integer, Integer> stops = new HashMap<>(); |
| |
| /** |
| * Parse delimiters from a string, discards any previously parsed |
| * information |
| * |
| * @param command |
| * Command string |
| */ |
| public void parseFrom(String command) { |
| this.starts.clear(); |
| this.stops.clear(); |
| |
| char[] cs = command.toCharArray(); |
| for (int i = 0; i < cs.length; i++) { |
| switch (cs[i]) { |
| case '"': |
| // Start of a Literal |
| // Is it a long literal? |
| if (i < cs.length - 2 && cs[i + 1] == '"' && cs[i + 2] == '"') { |
| this.addStart(i, "\"\"\""); |
| for (int j = i + 3; j < cs.length - 2; j++) { |
| if (cs[j] == '"' && cs[j + 1] == '"' && cs[j + 2] == '"') { |
| this.addStop(i, j + 2); |
| i = j + 2; |
| break; |
| } |
| } |
| } else { |
| // Normal literal, scan till we see a " which is not |
| // preceded by a \ |
| this.addStart(i, "\""); |
| for (int j = i + 1; j < cs.length; j++) { |
| if (cs[j] == '"' && cs[j - 1] != '\\') { |
| this.addStop(i, j); |
| i = j; |
| break; |
| } |
| } |
| } |
| break; |
| case '<': |
| // Start of a URI |
| this.addStart(i, "<"); |
| for (int j = i + 1; j < cs.length; j++) { |
| if (cs[j] == '>' && cs[j - 1] != '\\') { |
| this.addStop(i, j); |
| i = j; |
| break; |
| } |
| } |
| break; |
| case '\'': |
| // Start of alternative literal form |
| // Start of a Literal |
| // Is it a long literal? |
| if (i < cs.length - 2 && cs[i + 1] == '\'' && cs[i + 2] == '\'') { |
| this.addStart(i, "'''"); |
| for (int j = i + 3; j < cs.length - 2; j++) { |
| if (cs[j] == '\'' && cs[j + 1] == '\'' && cs[j + 2] == '\'') { |
| this.addStop(i, j + 2); |
| i = j + 2; |
| break; |
| } |
| } |
| } else { |
| // Normal literal, scan till we see a ' which is not |
| // preceded by a \ |
| this.addStart(i, "'"); |
| for (int j = i + 1; j < cs.length; j++) { |
| if (cs[j] == '\'' && cs[j - 1] != '\\') { |
| this.addStop(i, j); |
| i = j; |
| break; |
| } |
| } |
| } |
| break; |
| case '#': |
| // Start of a comment |
| // Scan to next newline |
| this.addStart(i, "#"); |
| for (int j = i + 1; j < cs.length; j++) { |
| if (cs[j] == '\n' || cs[j] == '\r') { |
| this.addStop(i, j); |
| i = j; |
| break; |
| } |
| } |
| this.addStop(i, cs.length - 1); |
| break; |
| case '\n': |
| case '\r': |
| case '.': |
| case ',': |
| case ';': |
| case '(': |
| case ')': |
| case '{': |
| case '}': |
| case '[': |
| case ']': |
| // Treat various punctuation as delimiters |
| this.addStart(i, new String(new char[] { cs[i] })); |
| this.addStop(i, i); |
| break; |
| } |
| } |
| } |
| |
| public void addStart(int index, String delim) { |
| this.starts.add(new Pair<>(index, delim)); |
| } |
| |
| public void addStop(int start, int stop) { |
| this.stops.put(start, stop); |
| } |
| |
| public Pair<Integer, String> findBefore(int index) { |
| Pair<Integer, String> found = null; |
| for (Pair<Integer, String> pair : this.starts) { |
| if (pair.getLeft() < index) |
| found = pair; |
| if (pair.getLeft() >= index) |
| break; |
| } |
| return found; |
| } |
| |
| public Pair<Integer, String> findAfter(int index) { |
| for (Pair<Integer, String> pair : this.starts) { |
| if (pair.getLeft() > index) |
| return pair; |
| } |
| return null; |
| } |
| |
| public boolean isInsideLiteral(int start, int stop) { |
| Pair<Integer, String> pair = this.findBefore(start); |
| if (pair == null) |
| return false; |
| if (pair.getRight().equals("\"")) { |
| Integer nearestStop = this.stops.get(pair.getLeft()); |
| if (nearestStop == null) |
| return true; // Inside unterminated literal |
| return (nearestStop > stop); // May be inside a literal |
| } else { |
| // Not inside a literal |
| return false; |
| } |
| } |
| |
| public boolean isInsideAltLiteral(int start, int stop) { |
| Pair<Integer, String> pair = this.findBefore(start); |
| if (pair == null) |
| return false; |
| if (pair.getRight().equals("'")) { |
| Integer nearestStop = this.stops.get(pair.getLeft()); |
| if (nearestStop == null) |
| return true; // Inside unterminated literal |
| return (nearestStop > stop); // May be inside a literal |
| } else { |
| // Not inside a literal |
| return false; |
| } |
| } |
| |
| public boolean isBetweenLiterals(int start, int stop) { |
| Pair<Integer, String> pairBefore = this.findBefore(start); |
| if (pairBefore == null) |
| return false; |
| if (pairBefore.getRight().equals("\"")) { |
| Integer stopBefore = this.stops.get(pairBefore.getLeft()); |
| if (stopBefore == null) |
| return false; // Inside unterminated literal |
| |
| // We occur after a literal, is there a subsequent literal? |
| Pair<Integer, String> pairAfter = this.findAfter(stop); |
| return pairAfter != null && pairAfter.getRight().equals("\""); |
| } else { |
| // Previous deliminator is not that of a literal |
| return false; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| for (Pair<Integer, String> pair : this.starts) { |
| builder.append("Delim "); |
| builder.append(pair.getRight()); |
| builder.append(" - Start "); |
| builder.append(pair.getLeft()); |
| builder.append(" - End "); |
| builder.append(this.stops.get(pair.getLeft())); |
| builder.append('\n'); |
| } |
| return builder.toString(); |
| } |
| |
| } |
| |
| /** |
| * Assign a VALUES valueName with a multiple items.<br> |
| * Can be used to assign multiple values to a single variable or single |
| * value to multiple variables (if using a List) in the SPARQL query.<br> |
| * See setRowValues to assign multiple values to multiple variables.<br> |
| * Using "valueName" with list(prop_A, obj_A) on query "VALUES (?p ?o) |
| * {?valueName}" * would produce "VALUES (?p ?o) {(prop_A obj_A)}". |
| * |
| * |
| * @param valueName |
| * @param items |
| */ |
| public void setValues(String valueName, Collection<? extends RDFNode> items) { |
| items.forEach(item -> validateParameterValue(item.asNode())); |
| |
| // Ensure that a list is used for the items. |
| Collection<List<? extends RDFNode>> rowItems = new ArrayList<>(); |
| if (items instanceof List) { |
| rowItems.add((List<? extends RDFNode>) items); |
| } else { |
| rowItems.add(new ArrayList<>(items)); |
| } |
| this.valuesReplacements.put(valueName, new ValueReplacement(valueName, rowItems)); |
| } |
| |
| /** |
| * Assign a VALUES valueName with a single item.<br> |
| * Using "valueName" with Literal obj_A on query "VALUES ?o {?valueName}" |
| * would produce * "VALUES ?o {obj_A}". |
| * |
| * @param valueName |
| * @param item |
| */ |
| public void setValues(String valueName, RDFNode item) { |
| setValues(valueName, Arrays.asList(item)); |
| } |
| |
| /** |
| * ** Sets a map of VALUES valueNames and their items.<br> |
| * Can be used to assign multiple values to a single variable or single |
| * value to multiple variables (if using a List) in the SPARQL query.<br> |
| * See setRowValues to assign multiple values to multiple variables. |
| * |
| * @param itemsMap |
| */ |
| public void setValues(Map<String, Collection<? extends RDFNode>> itemsMap) { |
| itemsMap.forEach(this::setValues); |
| } |
| |
| /** |
| * Allocate multiple lists of variables to a single VALUES valueName.<br> |
| * Using "valuesName" with list(list(prop_A, obj_A), list(prop_B, obj_B)) on |
| * query "VALUES (?p ?o) {?valuesName}" would produce "VALUES (?p ?o) |
| * {(prop_A obj_A) * (prop_B obj_B)}". |
| * |
| * @param valueName |
| * @param rowItems |
| */ |
| public void setRowValues(String valueName, Collection<List<? extends RDFNode>> rowItems) { |
| rowItems.forEach(collection -> collection.forEach(item -> validateParameterValue(item.asNode()))); |
| this.valuesReplacements.put(valueName, new ValueReplacement(valueName, rowItems)); |
| } |
| |
| private String applyValues(String command, SerializationContext context) { |
| |
| for (ValueReplacement valueReplacement : valuesReplacements.values()) { |
| command = valueReplacement.apply(command, context); |
| } |
| return command; |
| } |
| |
| private static final String VALUES_KEYWORD = "values"; |
| |
| protected static String[] extractTargetVars(String command, String valueName) { |
| String[] targetVars = new String[] {}; |
| |
| int valueIndex = command.indexOf(valueName); |
| if (valueIndex > -1) { |
| // Truncate the command at the valueName. |
| // Lowercase to search both cases of VALUES keyword. |
| String subCmd = command.substring(0, valueIndex).toLowerCase(); |
| int valuesIndex = subCmd.lastIndexOf(VALUES_KEYWORD); |
| int openBracesIndex = subCmd.lastIndexOf("{"); |
| int closeBracesIndex = subCmd.lastIndexOf("}"); |
| |
| // Ensure that VALUES keyword is found, open braces index is located |
| // after the VALUES and any close braces is located before the |
| // VALUES. |
| if (valuesIndex > -1 && valuesIndex < openBracesIndex && closeBracesIndex < valuesIndex) { |
| String vars = command.substring(valuesIndex + VALUES_KEYWORD.length(), openBracesIndex); |
| targetVars = vars.replaceAll("[(?$)]", "").trim().split(" "); |
| } |
| } |
| return targetVars; |
| } |
| |
| /** |
| * Performs replacement of VALUES in query string. |
| * |
| */ |
| private class ValueReplacement { |
| |
| private final String valueName; |
| private final Collection<List<? extends RDFNode>> rowItems; |
| |
| public ValueReplacement(String valueName, Collection<List<? extends RDFNode>> rowItems) { |
| this.valueName = valueName; |
| this.rowItems = rowItems; |
| } |
| |
| public String apply(String command, SerializationContext context) { |
| |
| if (rowItems.isEmpty()) { |
| return command; |
| } |
| |
| String[] targetVars = extractTargetVars(command, valueName); |
| if (targetVars.length == 0) { |
| // VALUES keyword has not been found or there is another issue |
| // so do not modify the command. |
| return command; |
| } |
| |
| validateValuesSafeToInject(command, targetVars); |
| |
| String target = createTarget(); |
| String replacement = buildReplacement(targetVars.length, context); |
| |
| return command.replaceAll(target, replacement); |
| } |
| |
| private String buildReplacement(int targetVarCount, SerializationContext context) { |
| |
| StringBuilder replacement = new StringBuilder(""); |
| |
| if (targetVarCount == 1) { |
| for (List<? extends RDFNode> row : rowItems) { |
| for (RDFNode item : row) { |
| replacement.append("("); |
| String insert = stringForNode(item.asNode(), context); |
| replacement.append(insert); |
| replacement.append(") "); |
| } |
| } |
| } else { |
| for (List<? extends RDFNode> row : rowItems) { |
| replacement.append("("); |
| for (RDFNode item : row) { |
| String insert = stringForNode(item.asNode(), context); |
| replacement.append(insert); |
| replacement.append(" "); |
| } |
| replacement.deleteCharAt(replacement.length() - 1); |
| replacement.append(") "); |
| } |
| } |
| |
| if (replacement.length() > 0) { |
| replacement.deleteCharAt(replacement.length() - 1); |
| } |
| |
| return replacement.toString(); |
| } |
| |
| /** |
| * Tidy up valueName if doesn't start with a ? or $. |
| * |
| * @param valueName |
| * @return |
| */ |
| private String createTarget() { |
| String target; |
| |
| if (valueName.startsWith("?") || valueName.startsWith("$")) { |
| target = valueName; |
| } else { |
| target = "[?$]" + valueName; |
| } |
| return target; |
| } |
| |
| protected void validateValuesSafeToInject(String command, String[] targetVars) { |
| |
| if (targetVars.length == 1) { |
| // Single var with one or more items so all checked against the |
| // same var. |
| String targetVar = targetVars[0]; |
| for (List<? extends RDFNode> row : rowItems) { |
| for (RDFNode item : row) { |
| validateSafeToInject(command, targetVar, item.asNode()); |
| } |
| } |
| } else { |
| // Multiple var with one or more rows. |
| for (int i = 0; i < targetVars.length; i++) { |
| String targetVar = targetVars[i]; |
| for (List<? extends RDFNode> row : rowItems) { |
| if (targetVars.length == row.size()) { |
| RDFNode item = row.get(i); |
| validateSafeToInject(command, targetVar, item.asNode()); |
| } else { |
| String rowString = row.stream().map(RDFNode::toString).collect(Collectors.joining(",")); |
| throw new ARQException("Number of VALUES variables (" + String.join(", ", targetVars) |
| + ") does not equal replacement row (" + rowString + ")."); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| } |