/**
 * 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.*;
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.irix.IRIx;
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(IRIx iri) {
        this.appendNode(NodeFactory.createURI(iri.toString()));
    }

    /**
     * Appends an IRI to the command text as a constant using appropriate
     * formatting
     *
     * @param iri
     *            IRI to append
     * @deprecated Use {@link #appendIri(IRIx)}
     */
    @Deprecated
    public void appendIri(org.apache.jena.iri.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
     * @deprecated Use {@link #setIri(int, IRIx)}
     */
    @Deprecated
    public void setIri(int index, org.apache.jena.iri.IRI iri) {
        this.setIri(index, 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 iri
     *            IRI
     */
    public void setIri(int index, IRIx 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, IRIx iri) {
        this.setIri(var, 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
     */
    @Deprecated
    public void setIri(String var, org.apache.jena.iri.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 + ").");
                        }
                    }
                }
            }
        }
    }

}
