Chapter 3. Using XPath

Table of Contents

1. The two implementations of XPath contained in XMLmind XML Editor
2. Compiling and running the sample program
3. Compiling and evaluating an XPath expression
3.1. XNode
3.2. Two steps: parse and evaluate
3.3. ExprContext
4. XPath value types
4.1. evalAsNumber
4.2. evalAsBoolean
4.3. evalAsNodeSet
4.4. evalAsVariant
5. Matching a node against an XPath pattern

In the preceding chapter, we have studied how to program XMLmind XML Editor DOM. You may have noticed that there is no easy way to do something as simple as "fetch the element having a given ID". This is where XPath comes in. If you need to fetch the element having a given ID, you'll have to evaluate XPath expression "id('foo')" (in this example, searched ID is foo).

1. The two implementations of XPath contained in XMLmind XML Editor

XMLmind XML Editor contains two implementations of XPath:

The XPath implementation has a rather low-level API. Therefore the first reasonable thing to do when you need to evaluate XPath expressions is to write a few convenience functions. This is exactly what we are going to do in this tutorial. And by implementing the convenience functions, we will study the low-level XPath API.

XPathUtil.java contains a set of static methods such as evalAsString which are convenience functions.

Note that XMLmind XML Editor contains its own version of XPathUtil.java[1]: com.xmlmind.xmledit.xpath.XPathUtil. Therefore, after studying this tutorial, you'll be able to use all these convenience functions right away; that is, without having to dynamically load any extension jar.

2. Compiling and running the sample program

  • Compile XPathUtil by executing ant (see build.xml) in the samples/xpath_util/ directory.

  • Run XPathUtil by executing ant run in the samples/xpath_util/ directory. This small test program searches sample1.html for all ordered lists having a decimal numbering style ("//ol[contains(@style,'decimal')]").

3. Compiling and evaluating an XPath expression

3.1. XNode

public static String evalAsString(String expr, XNode node) 
    throws ParseException, EvalException {
    return evalAsString(expr, node, null);
}

The goal of the above method is to evaluate XPath expression expr (example: "//ol[contains(@style,'decimal')]"), using node as the initial context. The result of the evaluation is to be converted to a string (example: "2.0 + 2.0" is converted to string "4") using the rules described in the XPath spec.

Note that the context node is a com.xmlmind.xml.doc.XNode and not any of the Document, Element, Comment, etc, Nodes we have studied in previous chapter.

XMLmind XML Editor DOM uses Nodes. Each kind of Node has a different API. For example, Element has a getName method and Text has a getText method but no getName method. And in XMLmind XML Editor DOM, for efficiency reasons, attributes are not Nodes.

XPath prefers to deal with uniform XNodes. For example, all types of XNodes should support the name method, even this does not make sense in the case of text nodes, comment nodes, document nodes, etc. And, unlike in XMLmind XML Editor DOM, attributes too are XNodes.

All XMLmind XML Editor DOM Nodes implement the XNode interface. This means that you can pass, for example, an Element as the context node of evalAsString without even thinking about it.

Important

Unless you are using XXE XPath facilities, do not program against the XNode interface. Instead program against XXE native DOM, that is, Node, Document, Element, etc.

3.2. Two steps: parse and evaluate

The above method is in fact a convenience function for the slightly lower-level method below:

public static String evalAsString(String expr, XNode node,
                                  Object[] variables) 
    throws ParseException, EvalException {
    StringExpr parsed = 
        ExprParser.parseStringExpr(expr, node.namespacePrefixMap());
    return parsed.eval(node, createExprContext(variables));
}

First step consists in parsing the XPath string as a StringExpr. This step may throw a ParseException.

Second step consists in evaluating the parsed XPath expression, a StringExpr, in the context of node, using an ExprContext as the ``evaluation environment'' (more on this in next section). This step may throw an EvalException.

A parsed XPath expression can of course be stored in order to be evaluated several times.

When parsing an expression such as "/c:description/x:div", the parser needs to know the namespaces corresponding to "c" and to "x"[2]. To do this, the parser uses an object which implements the com.xmlmind.xmledit.xmlutil.PrefixToNamespace interface. Method namespacePrefixMap returns such object.

3.3. ExprContext

private static ExprContext createExprContext(Object[] variables) {
    ExprContextImpl exprContext = new ExprContextImpl();

    int variableCount;
    if (variables != null &&
        (variableCount = variables.length) > 0) {
        variableCount = 2*(variableCount/2);
        for (int i = 0; i < variableCount; i += 2) {
            Object name = variables[i];
            Object value = variables[i+1];

            Name varName = null;
            if (name instanceof Name) {
                varName = (Name) name;
            } else if ((name instanceof String) &&
                       XMLText.isNCName((String) name)) {
                varName = Name.get((String) name);
            }

            if (varName != null) {
                Variant varValue;
                if (value instanceof Variant) {
                    varValue = (Variant) value;
                } else {
                    // Supports value==null
                    varValue = VariantBase.create(value);
                }

                exprContext.setGlobalVariable(varName, varValue);
            }
        }
    }

    return exprContext;
}

In some cases, the XPath expression you want to evaluate contains references to variables. Example: "//ulink[@url=$home.url]". In such case, the XPath evaluator gets the values of theses variables from the ``evaluation environment'' passed to parseStringExpr.

ExprContext is an interface implemented by XPath evaluation environments. It specifies the following services:

  • Access to variables referenced in XPath expressions.

  • Maps elements to IDs.

  • Access to external documents (used to implement standard XSLT function document()).

ExprContextImpl is a simple implementation of ExprContext, which implements all the services, except the access to variables. That's why we had to implement createExprContext. This method uses setGlobalVariable to map the name of the variable to its value: a Variant (more on this in next section).

An ExprContextImpl can be reused several times, unless the documents which are searched using XPath expressions have been modified. In such case, make sure to create and use a new ExprContextImpl or if you prefer, invoke method reset before reusing a ExprContextImpl.

4. XPath value types

XPath is a dynamically typed language which can automatically convert values between the four following types:

The XPath spec defines rules to convert from almost any value type to almost any other value type. However, the spec (and common sense) says that some conversions simply cannot work: number 3.14 to a node-set, string "foo" to a node-set, etc.

4.1. evalAsNumber

If you know in advance that the XPath expression will return a number, you may want to use evalAsNumber rather than evalAsString. evalAsNumber is implemented using NumberExpr and parseNumberExpr.

public static double evalAsNumber(String expr, XNode node,
                                  Object[] variables) 
    throws ParseException, EvalException {
    NumberExpr parsed = 
        ExprParser.parseNumberExpr(expr, node.namespacePrefixMap());
    return parsed.eval(node, createExprContext(variables));
}

4.2. evalAsBoolean

If you know in advance that the XPath expression will return a boolean, you may want to use evalAsBoolean rather than evalAsString. evalAsBoolean is implemented using BooleanExpr and parseBooleanExpr.

public static boolean evalAsBoolean(String expr, XNode node,
                                    Object[] variables) 
    throws ParseException, EvalException {
    BooleanExpr parsed = 
        ExprParser.parseBooleanExpr(expr, node.namespacePrefixMap());
    return parsed.eval(node, createExprContext(variables));
}

4.3. evalAsNodeSet

If you know in advance that the XPath expression will return XNodes, you'll have to use evalAsNodeSet rather than evalAsString. evalAsNodeSet is implemented using NodeSetExpr and parseNodeSetExpr.

public static XNode[] evalAsNodeSet(String expr, XNode node,
                                    Object[] variables) 
    throws ParseException, EvalException {
    NodeSetExpr parsed = 
        ExprParser.parseNodeSetExpr(expr, node.namespacePrefixMap());
    NodeIterator iter = parsed.eval(node, createExprContext(variables));

    ArrayList<XNode> list = new ArrayList<XNode>();
    for (;;) {
        XNode n = iter.next();
        if (n == null) {
            break;
        }

        list.add(n);
    }

    XNode[] nodes = new XNode[list.size()];
    list.toArray(nodes);
    return nodes;
}

A NodeIterator is always used like this:

    for (;;) {
        XNode n = iter.next();
        if (n == null) {
            break;
        }

         /* Use XNode n. */
    }

The important thing to remember is that a NodeIterator can only be used once to iterate over the node-set it represents[3]. If you intend to store a NodeIterator in order to reuse it several times, you need to:

  1. Parse the expression as a VariantExpr.

  2. Evaluate the VariantExpr.

  3. Invoke method makePermanent on the Variant returned by eval.

  4. Store the Variant returned by makePermanent.

  5. Invoke method convertToNodeSet on the stored Variant each time you need to iterate over the node-set.

4.4. evalAsVariant

If you don't know in advance what the XPath expression will return, you'll have to use evalAsVariant rather than evalAsString. evalAsVariant is implemented using VariantExpr and parseVariantExpr.

public static Variant evalAsVariant(String expr, XNode node,
                                    Object[] variables) 
    throws ParseException, EvalException {
    VariantExpr parsed = 
        ExprParser.parseVariantExpr(expr, node.namespacePrefixMap());
    return parsed.eval(node, createExprContext(variables));
}

Variant represents a dynamically typed value manipulated by the XPath evaluator. This interface has methods to query the type of the value (example: isNumber) and methods to convert between different value types (example: convertToBoolean).

5. Matching a node against an XPath pattern

public static boolean matches(XNode node, String pattern) 
    throws ParseException, EvalException {
    return matches(node, pattern, null);
}

public static boolean matches(XNode node, String pattern,
                              Object[] variables) 
    throws ParseException, EvalException {
    Pattern parsed = 
        ExprParser.parsePattern(pattern, node.namespacePrefixMap());
    return parsed.matches(node, createExprContext(variables));
}

Matching a node against an XPath pattern is a problem which is totally different from finding nodes in a document subtree. That's why a different representation is used for that, a Pattern (and not a StringExpr, NumberExpr, etc).

The important thing to remember here is that all valid XPath expressions are not valid XPath patterns. Valid XPath patterns are defined in the XSLT spec.

This being said, as you can see in the above code, using a Pattern is similar (and even simpler) to using a StringExpr, BooleanExpr, etc.



[1] Identical to the one described in this tutorial, except that the class is documented using Javadoc™ and that the test main() has been removed.

[2] Remember that the default namespace cannot be used with XPath. For example, even if default namespace is "http://www.w3.org/1999/xhtml", "//p" means "//{}p" and not "//{http://www.w3.org/1999/xhtml}p".

[3] Assume that any method of a NodeIterator ``consumes'' it. Example: invoking toString() once on a NodeIterator will almost certainly makes it unusable for any other task.