blob: b77b4de4d61fd3dc49f5e3437c2a0d326741646c [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jena.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 + ").");
}
}
}
}
}
}
}