How to use Java String variables inside XPath query

13,175

Solution 1

String rawXPath = "//book[author= '" + larrysName + "']/title/text()";

or

String rawXPath = String.format("//book[author= '%s']/title/text()", larrysName);

where larrysName is a variable of type String coming from somewhere.

Solution 2

The problem here is when you have an author like the infamous Larry "Basher" O'Niven.

In this case, you will need to escape the variable, as in this naive implementation:

  public static String escape(String s) {
    Matcher matcher = Pattern.compile("['\"]")
        .matcher(s);
    StringBuilder buffer = new StringBuilder("concat(");
    int start = 0;
    while (matcher.find()) {
      buffer.append("'")
          .append(s.substring(start, matcher.start()))
          .append("',");
      buffer.append("'".equals(matcher.group()) ? "\"'\"," : "'\"',");
      start = matcher.end();
    }
    if (start == 0) {
      return "'" + s + "'";
    }
    return buffer.append("'")
        .append(s.substring(start))
        .append("'")
        .append(")")
        .toString();
  }

This can be demonstrated with this code:

String xml =
    "<xml><foo bar=\"Larry &quot;Basher&quot; O'Niven\">Ringworm</foo></xml>";
String query =
    String.format("//foo[@bar=%s]", escape("Larry \"Basher\" O'Niven"));
System.out.println(query);
String book = XPathFactory.newInstance()
    .newXPath()
    .evaluate(query, new InputSource(new StringReader(xml)));
System.out.println(query + " > " + book);

Solution 3

You can in fact use both custom functions and variables in XPath -but a quick hack might be more productive for many uses.

Below is some code I developed as a learning tool for our students. It lets you do this:

// create some variable we want to use in the xpath
xPathVariableAndFunctionResolver.newVariable("myNamespace", "id", "xs:string", "l2"); // myNamespace is declared in the namespace context with prefix 'my'

// create an XPath expression
String expression = "//did:Component[@id=$my:id]"; // variable $namespace:name
XPathExpression findComponents = xPathFunctionAndVariableOperator.compile(expression);

// execute the XPath expression against the document
NodeList statements = (NodeList)findComponents.evaluate(document, XPathConstants.NODESET);

And much the same with XPath functions. The code, first a wrapper for the normal XPath evalutation:

public class XPathOperator {

    protected XPath xPath;
    protected XPathFactory xPathFactory;

    private Hashtable<String, XPathExpression> compiled = new Hashtable<String, XPathExpression>();

    protected void initFactory() throws XPathFactoryConfigurationException {
        xPathFactory = XPathFactory.newInstance(XPathConstants.DOM_OBJECT_MODEL);
    }

    protected void initXPath(NamespaceContext context) {
        xPath = xPathFactory.newXPath();
        xPath.setNamespaceContext(context);
    }

    public XPathOperator(NamespaceContext context) throws XPathFactoryConfigurationException {
        initFactory();
        initXPath(context);
    }

    public Object evaluate(Document document, String expression, QName value) throws XPathExpressionException {

        // create an XPath expression - http://www.zvon.org/xxl/XPathTutorial/General/examples.html
        XPathExpression findStatements = compile(expression);

        // execute the XPath expression against the document
        return (NodeList)findStatements.evaluate(document, value);
    }

    public XPathExpression compile(String expression) throws XPathExpressionException {
        if(compiled.containsKey(expression)) {
            return (XPathExpression) compiled.get(expression);
        }

        XPathExpression xpath = xPath.compile(expression);

        System.out.println("Compiled XPath " + expression);

        compiled.put(expression, xpath);

        return xpath;
    }
}

Then we add the concept of custom variables and functions, of course with namespaces:

public class XPathFunctionAndVariableOperator extends XPathOperator {

        public XPathFunctionAndVariableOperator(NamespaceContext context, XPathVariableResolver xPathVariableResolver, XPathFunctionResolver xPathFunctionResolver) throws XPathFactoryConfigurationException {

    super(context);

        xPath.setXPathVariableResolver(xPathVariableResolver);
        xPath.setXPathFunctionResolver(xPathFunctionResolver);
    }
}

Which would not be much fun without the variable and function resolvers:

public class XPathVariableAndFunctionResolver implements XPathVariableResolver, XPathFunctionResolver {

    private Hashtable functions = new Hashtable();
    private Hashtable variables = new Hashtable();

    private SchemaDVFactory factory = SchemaDVFactory.getInstance();

    public XPathFunction resolveFunction(QName functionName, int arity) {
        Hashtable table = (Hashtable)functions.get(functionName.getNamespaceURI());
        if(table != null) {
            XPathFunction function = (XPathFunction)table.get(functionName.getLocalPart());
            if(function == null) {
                throw new RuntimeException("Function " + functionName.getLocalPart() + " does not exist in namespace " + functionName.getNamespaceURI() + "!");
            }
            System.out.println("Resolved function " + functionName + " with " + arity + " argument(s)");
            return function;
        }
        throw new RuntimeException("Function namespace " + functionName.getNamespaceURI() + " does not exist!");
    }

    /**
     *
     * Adds a variable using namespace and name, primitive type and default value
     *
     * @param namespace
     * @param name
     * @param datatype      one of the built-in XML datatypes
     * @param value
     * @throws InvalidDatatypeValueException    if value is not of correct datatype
     */

    @SuppressWarnings("unchecked")
    public void newVariable(String namespace, String name, String datatype, String value) throws InvalidDatatypeValueException {

        int index = datatype.indexOf(":");
        if(index != -1) {
            datatype = datatype.substring(index+1);
        }
        XSSimpleType builtInType = factory.getBuiltInType(datatype);

        if(builtInType == null) {
            throw new RuntimeException("Null type for " + datatype);
        }

        ValidationState validationState = new ValidationState();
        ValidatedInfo validatedInfo = new ValidatedInfo();

        builtInType.validate(value, validationState, validatedInfo);

        System.out.println("Defined variable " + name + " as " + datatype + " with value " + value);

        Hashtable table;
        if(!variables.containsKey(namespace)) {
            table = new Hashtable();
            variables.put(namespace, table);
        } else {
            table = (Hashtable)variables.get(namespace);
        }

        table.put(name, new Object[]{validatedInfo, builtInType});
    }

    public void newVariableValue(String namespace, String name, String value) throws InvalidDatatypeValueException {
        ValidationState validationState = new ValidationState();

        Hashtable table;
        if(!variables.containsKey(namespace)) {
            throw new RuntimeException("Unknown variable namespace " + namespace);
        } else {
            table = (Hashtable)variables.get(namespace);
        }

        Object[] bundle = (Object[])table.get(name);
        ValidatedInfo validatedInfo = (ValidatedInfo)bundle[0];
        XSSimpleType builtInType =  (XSSimpleType)bundle[1];
        builtInType.validate(value, validationState, validatedInfo); // direct reference transfer of value

        System.out.println("Assigned value " + validatedInfo.normalizedValue + " to variable " + name);
    }

    public Object resolveVariable(QName variableName) {

        Hashtable table;
        if(!variables.containsKey(variableName.getNamespaceURI())) {
            throw new RuntimeException("Unknown variable namespace " + variableName.getNamespaceURI());
        } else {
            table = (Hashtable)variables.get(variableName.getNamespaceURI());
        }

        Object[] bundle = (Object[])table.get(variableName.getLocalPart());
        if(bundle != null) {
            ValidatedInfo var = (ValidatedInfo)bundle[0];

            if(var != null) {
                switch(var.actualValueType) { // some types omitted, customize your own
                case XSConstants.INTEGER_DT:
                case XSConstants.DECIMAL_DT:
                case XSConstants.INT_DT:
                case XSConstants.LONG_DT:
                case XSConstants.SHORT_DT:
                case XSConstants.BYTE_DT:
                case XSConstants.UNSIGNEDBYTE_DT:
                case XSConstants.UNSIGNEDINT_DT:
                case XSConstants.UNSIGNEDLONG_DT:
                case XSConstants.UNSIGNEDSHORT_DT:
                    return new Integer(var.normalizedValue);
                case XSConstants.DATE_DT:
                case XSConstants.DATETIME_DT:
                case XSConstants.GDAY_DT:
                case XSConstants.GMONTH_DT:
                case XSConstants.GMONTHDAY_DT:
                case XSConstants.GYEAR_DT:
                case XSConstants.GYEARMONTH_DT:
                case XSConstants.DURATION_DT:
                case XSConstants.TIME_DT:
                    return new Date(var.normalizedValue);
                case XSConstants.FLOAT_DT:
                    return new Float(Float.parseFloat(var.normalizedValue));
                case XSConstants.DOUBLE_DT:
                    return new Double(Double.parseDouble(var.normalizedValue));
                case XSConstants.STRING_DT:
                case XSConstants.QNAME_DT:
                    return var.normalizedValue;
                default:
                    throw new RuntimeException("Unknown datatype " + var.actualValueType + " for variable " + variableName + " in namespace " + variableName.getNamespaceURI());
                }
            }
        }
        throw new RuntimeException("Could not resolve value " + variableName + " in namespace " + variableName.getNamespaceURI());
    }

    public void addFunction(String namespace, String name, XPathFunction function) {
        Hashtable table;
        if(!functions.containsKey(namespace)) {
            table = new Hashtable();
            functions.put(namespace, table);
        } else {
            table = (Hashtable)functions.get(namespace);
        }
        table.put(name, function);
    }

}

The functions obviously cannot be contained within the above, since typically running custom code (i.e. the whole point is that you write your own class), so go with something like

public abstract class XPathFunctionImpl implements XPathFunction {

    /**
     * This function is called by the XPath expression as it implements the interface XPathFunction
     */

    protected int numberArguments;

    public Object evaluate(List args) throws XPathFunctionException {
        if(args.size() == numberArguments) {
            return evaluateImpl(args);
        }
        throw new RuntimeException("Illegal number of arguments for " + this);
    }

    public abstract Object evaluateImpl(List args) throws XPathFunctionException;

}

And then the implement/subclass your own logic in evaluateImpl(..) somehow.

This sure makes the String appending seem quite ... attractive ;) Note: This code is several years old and there might exist a better way of doing all this.

Share:
13,175
JavaBits
Author by

JavaBits

Updated on June 04, 2022

Comments

  • JavaBits
    JavaBits about 2 years

    I have books.xml file which contains author name and book titles. I am using the following code snippet to query books.xml.

    XPathFactory factory = XPathFactory.newInstance();
    XPath xpath = factory.newXPath();
    XPathExpression expr 
        = xpath.compile("//book[author= 'Larry Niven']/title/text()");
    

    Now instead of directly putting the name in the query if I want to pass it while the program is running as a String variable how to do it. Just putting the string variable name is not working!

  • McDowell
    McDowell about 13 years
    -1 unfortunately, this will not work perfectly for names like O'Hara or any other text that includes quotes.
  • ntoskrnl
    ntoskrnl almost 11 years
    -1. It's also vulnerable to XPath injection for the same reason – probably not as bad as SQL injection in practice, but based on the same principle.