| kdgregory.com | |
|
Blog
Food Programming Travel |
Practical XML: XPath An XPath Primer XPath is a way to navigate through an XML document, modeled on filesystem
paths. An XPath expression consistes of a series of “node tests”
that describe how to traverse from one element to another within a document.
For example, given the following XML, the path “
<foo>
<bar name='argle'>
Argle content
</bar>
<bar name='bargle'>
Bargle content
<baz>Baz content</baz>
</bar>
</foo>
Like a filesystem path, XPaths can be relative to a given node: the path
“ Unlike filesystem paths, however, an XPath can specify a predicate:
a logical test that's applied to the set of nodes selected at that point in
the path. For example, the XML fragment above has two nodes named
“ The XPath specification will tell you all of the variants of an XPath expression. This article is about how to evaluate XPaths in Java. So here's the code to execute the first example above:
Document dom = // however you get the document
XPath xpath = XPathFactory.newInstance().newXPath();
String result = xpath.evaluate("/foo/bar/baz", dom);
Pretty simple, eh? Like everything in the Result Types The first piece of complexity is what, exactly, you get when evaluating
an XPath expression. The simple answer is that it returns the list of nodes
that match the path and for which all predicates evaluate true, in document
order. Yet the example above returns a string value. And what happens
with a path like “ The answer is that the XPath specification requires any result be convertible
into a single string value. The value of a single node is the text contained
within that node and its descendents, and the string value of a list of nodes
is the string value of the first node in that list. In many cases, the string
value of an expression is all that you need, which is why the basic
If you need more control over your results, such as getting the actual
nodes selected, there's a form of
NodeList nodes = (NodeList)xpath.evaluate("/foo/bar", dom, XPathConstants.NODESET);
Note that the result type name is The term “nodeset” is confusing in several ways. First, it is
most definitely not a Nor is it a NamespacesNamespaces are perhaps the biggest source of pain in working with XPath. To see why, consider the following XML. It looks a lot like the example at the top of this article, with the addition of a default namespace. You might think that the example XPath expressions would work unchanged. You'd be wrong.
<foo xmlns='http://www.example.com'>
<bar name='argle'>
Argle content
</bar>
<bar name='bargle'>
Bargle content
<baz>Baz content</baz>
</bar>
</foo>
The reason: XPath is fully namespace aware, and node tests match both
localname and namespace. If you don't specify a namespace in the path,
the evaluator assumes that the node doesn't have a namespace. Making life
difficult, there's no way to explicitly specify a namespace as part of the
node test; you must instead use a “qualified name” name
( So, to select node
xpath.setNamespaceContext(
new SimpleNamespaceResolver("ns", "http://www.example.com"));
One last thing to remember about namespaces and qualified names: the
prefix doesn't matter. It's just a way to find the actual namespace URI
in a lookup table. The prefixes in your XPath expression don't have to match
those in the source document. As shown above, the XPath expression has to
use a prefix even when the source XML doesn't. Similarly, the source XML
could have a node named “ VariablesSo far, all of the examples have used literal strings containing the XPath expression. Doing this opens the door for hackers: an XPath expression built directly from client data is subject to “XPath injection,” similar to the better-known attacks against SQL. And, just as most SQL injection attacks can be thwarted through use of parameterized prepared statements rather than literal SQL, XPath expressions can be protected through the use of variables. XPath variables appear in an expression as a dollar-sign followed by a
name (which may or may not have a namespace prefix). For example,
“
xpath.setXPathVariableResolver(new XPathVariableResolver()
{
public Object resolveVariable(QName variableName)
{
Every time the XPath evaluator finds a variable reference, it will call
the resolver for a value. How you implement the resolver is up to you; a
simple One point bears repeating: the resolver is given a qualified name.
The variable in “ Functions Functions in an XPath expression look a lot like functions in other
programming languages: a name, followed by a list of arguments in
parentheses. An argument could be literal text, another function call,
or (in many cases) an XPath expression. For example, the following
expression uses the built-in translate(string(/foo/bar/baz), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') This expression is, quite frankly, ugly: it buries the node selection in
a mass of text. It's also flawed, in that it translates a limited set of
characters into another limited set of characters. If we need to uppercase
text, it would be better to use the Java method The first part of implementing a user-defined function is easy: implement
the function. Actually, it's not so easy: XPath functions are instances of
the
final XPathFunction myFunc = new XPathFunction()
{
public Object evaluate(List args)
throws XPathFunctionException
{
Object arg = args.iterator().next();
if (arg instanceof NodeList)
{
NodeList nodes = (NodeList)arg;
if (nodes.getLength() == 0)
return "";
else
return nodes.item(0).getTextContent().toUpperCase();
}
else if (arg instanceof String)
{
return ((String)arg).toUpperCase();
}
else
throw new XPathFunctionException("invalid argument: " + arg);
}
};
For the example, I created the function as an anonymous inner class. In a real application, I strongly recommend creating normal, named classes, and unit tests for them. Once you've implemented your functions, you have to create an
final QName myFuncName = new QName("foo", "uppercase");
xpath.setXPathFunctionResolver(new XPathFunctionResolver()
{
public XPathFunction resolveFunction(QName functionName, int arity)
{
if (myFuncName.equals(functionName) && (arity == 1))
return myFunc;
else
return null;
}
});
xpath.setNamespaceContext(new SimpleNamespaceResolver("ns", myFuncName.getNamespaceURI()));
There's a lot of stuff happening here so let's jump into the middle, the
function resolver itself. Whenever the XPath evaluator sees a reference
to a function that's not defined by the XPath spec it calls its resolver,
passing the name of the function and the number of arguments that actually
appear in the expression (don't blame me for that parameter
name, it
comes from the JavaDoc). In our case, we can handle only a single argument;
if the expression contains more or fewer, we return Note that the function name is passed as a After you've done all this, you can use an expression that references your function:
xpath.evaluate("ns:uppercase(/foo/bar/baz)", dom)
As you might guess, the Practical XML library has a few classes to make
this process easier. There's an implementation of
To be honest, I believe that XPath functions are often more trouble than they're worth. While there can be a lot of value in functions that are used in predicates, I would think twice before writing a function that does some aggregation of a nodeset. Instead, think about using an XPath to create the nodeset, then executing the function on that nodeset as part of your Java execution flow. XPathExpression All of the examples so far have called the
XPathExpression compiled = xpath.compile("/foo/bar/baz");
// ...
String value = compiled.evaluate(dom);
The XPathWrapper Although the XPath interfaces provided with the JDK are individually
simple, I find them painful in practice. Particularly when using
namespaces. The fact that most of this pain comes from boilerplate
code led to the development of
new XPathWrapper("/ns1:foo/ns2:bar[@ns2:name=$myvar]")
.bindNamespace("ns1", "http://foo.example.com")
.bindNamespace("ns2", "http://bar.example.com")
.bindVariable("myvar", "argle")
.evaluateAsString(dom2));
JxPathWhile this article has focused on the JDK's implementation of XPath, it is not the only implementation available. Of the alternatives, Apache Commons JxPath is notable because it allows XPath expressions to be used with arbitrary object graphs, not just XML. Out of the box it supports bean-style objects, JDK collections, and the context objects provided to J2EE servlets. While all that is useful, what's nicer is that JxPath is easy to extend. For example, you implement functions using a single class, none of which need to be attached to a namespace. Once you register the class, JxPath will use reflection to find the function. You can also easily extend JxPath to access arbitrary structured data types. I used it for a project where we stored content in the database as a collection of IDs: each unique string had its own ID, significantly reducing the storage of duplicate data. Since the actual data tables were just collections of IDs, we used JxPath to implement human-readable queries, by adding an override to JxPath's node test and navigation code. XPath as Debugging ToolOne of the problems of working with a DOM document is tracing issues back to their source. It's one reason that I recommend validating at the time of parsing, because once the parsing is done you've lost any source line or column numbers. However, you can create an XPath expression that will uniquely identify any node in an XML document, using predicates to differentiate nodes with the same name. There are some caveats when doing so: in particular, keeping track of namespaces and providing any new bindings to the calling code. Not surprisingly, the Practical XML library provides methods to do just this:
For more informationThe XPath 1.0 spec is actually pretty readable as W3C specs go. The Practical XML library provides utilities for working with XML in many different ways. All of the code fragments from this article are found in a single example program. Copyright © Keith D Gregory, all rights reserved |