Chapter 5. Using XPath

Table of Contents

1. The two implementations of XPath contained in XMLmind XML Editor
2. Compiling and evaluating an XPath expression
2.1. XNode
2.2. Two steps: parse and evaluate
2.3. ExprContext
3. XPath value types
3.1. evalAsNumber
3.2. evalAsBoolean
3.3. evalAsNodeSet
3.4. evalAsVariant
4. 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 very small subset needed to implement W3C XML Schema validation: com.xmlmind.xmledit.doc.XPath, Path, Step.

    Don't use this implementation. Though being a public and stable API, in the future, this implementation may be moved to where it belongs: the W3C XML Schema validation engine.

  • XPath 1.0 full implementation taken from James Clark's XT and adapted by XMLmind to the XMLmind XML Editor DOM. This implementation is found in package com.xmlmind.xmledit.xpath.

    This tutorial describes the XPath 1.0 full implementation.

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.

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

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

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 evaluating an XPath expression

2.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.xmledit.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.

2.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,
                                      HashMap variables) 
        throws ParseException, EvalException {
        StringExpr parsed = 
            ExprParser.parseStringExpr(expr, node.namespacePrefixMap(),
                                       /*acceptUnknownPrefix*/ true);
        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.

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

What if a prefix such as "x" is unknown? Unless parameter acceptUnknownPrefix of parseStringExpr is true, the parser will throw a ParseException. In XMLmind XML Editor, documents conforming to a DTD are not namespace aware, therefore it is strongly recommended to always pass true for this parameter, even if this will cause the parser to catch less errors.

2.3. ExprContext

    private static ExprContext createExprContext(final HashMap variables) {
        if (variables == null) {
            return new ExprContextBase();
        } else {
            return new ExprContextBase() {
                public Variant getVariableValue(Name name, XNode node) 
                    throws EvalException {
                    Object value = variables.get(name.getLocalPart());
                    if (value == null)
                        return null;
                    else
                        return VariantBase.create(value);
                }
            };
        }
    }

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()).

ExprContextBase is a simple implementation of ExprContext, which implements all the services, except the access to variables. That's why an anonymous class derived from ExprContextBase is created in createExprContext. This class redefines method getVariableValue. The implementation of this method uses a HashMap to map the local name of the variable to its value: a Variant (more on this in next section).

A ExprContextBase 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 ExprContextBase or if you prefer, invoke method reset before reusing a ExprContextBase.

3. 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.

3.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,
                                      HashMap variables) 
        throws ParseException, EvalException {
        NumberExpr parsed = 
            ExprParser.parseNumberExpr(expr, node.namespacePrefixMap(),
                                       /*acceptUnknownPrefix*/ true);
        return parsed.eval(node, createExprContext(variables));
    }

3.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,
                                        HashMap variables) 
        throws ParseException, EvalException {
        BooleanExpr parsed = 
            ExprParser.parseBooleanExpr(expr, node.namespacePrefixMap(),
                                        /*acceptUnknownPrefix*/ true);
        return parsed.eval(node, createExprContext(variables));
    }

3.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,
                                        HashMap variables) 
        throws ParseException, EvalException {
        NodeSetExpr parsed = 
            ExprParser.parseNodeSetExpr(expr, node.namespacePrefixMap(),
                                        /*acceptUnknownPrefix*/ true);
        NodeIterator iter = parsed.eval(node, createExprContext(variables));

        ArrayList list = new ArrayList();
        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.

3.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,
                                        HashMap variables) 
        throws ParseException, EvalException {
        VariantExpr parsed = 
            ExprParser.parseVariantExpr(expr, node.namespacePrefixMap(),
                                        /*acceptUnknownPrefix*/ true);
        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).

4. 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,
                                  HashMap variables) 
        throws ParseException, EvalException {
        Pattern parsed = 
            ExprParser.parsePattern(pattern, node.namespacePrefixMap(),
                                    /*acceptUnknownPrefix*/ true);
        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.