The Saxon API only works with Saxon. The Xalan API only works with Xalan. Both only work with Java. The W3C DOM Working Group is attempting to define a standard, cross-engine XPath API that can be used with many different XPath engines (though as of Summer 2002 this effort is just beginning and is not yet supported by any implementations). DOM Level 3 includes an optional XPath module in the org.w3c.dom.xpath package. The feature string "XPath" with the version "3.0" tests for the presence of this module. For example,
if (!impl.hasFeature("XPath", "3.0")) { System.err.println("This DOM implementation does not support XPath"); return; }
This section is based on the March 28, 2002, working draft of the Document Object Model (DOM) Level 3 XPath Specification. The details are still subject to change, however.
The XPath module has two main interfaces, XPathEvaluator and XPathResult. XPathEvaluator, shown in Example 16.6 searches an XML document for the objects identified by an XPath expression such as /book/chapter/section[starts-with(@title, 'DOM')]. The XPath expression is passed as a Java String, the context node as a DOM Node object. The result of evaluating the expression is returned as an XPathResult, a wrapper interface for the four standard XPath data types: node-set, string, boolean, and number.
Example 16.6. The XPathEvaluator interface
package org.w3c.dom.xpath; public interface XPathEvaluator { public XPathResult evaluate(String expression, Node contextNode, XPathNSResolver resolver, short type, XPathResult result) throws XPathException, DOMException; public XPathExpression createExpression(String expression, XPathNSResolver resolver) throws XPathException, DOMException; public XPathNSResolver createNSResolver(Node nodeResolver); }
In DOM implementations that support XPath, the same classes that implement org.w3c.dom.Document implement XPathEvaluator. Thus no special constructor or factory class is required. Just cast the Document object that encapsulates the document you want to query to XPathEvaluator (after checking to make sure that the implementation supports XPath with hasFeature(), of course). For example, in Chapter 5 you saw an XML-RPC server that returned Fibonacci numbers. The documents that server returned looked like this:
<?xml version="1.0"?> <methodResponse> <params> <param> <value><double>28657</double></value> </param> </params> </methodResponse>
A client for this program needs to extract the content of the double element. You can use XPath to simplify this task. There are numerous XPath expressions that will retrieve the relevant node. These include:
/methodResponse/params/param/value/double
/child::methodResponse/child::params/child::param/child::value/child::double[1]
/methodResponse/params/param/value/double[1]
//double[1]
/descendant::double[1]
Depending on what you intend to do with the node once you have it, you might want to use one of the functions that returns the string-value of the node instead. In that case, these expressions would be appropriate:
normalize-space(/methodResponse/params/param/value/double)
normalize-space(//double[1])
string(//double)
normalize-space(/methodResponse)
normalize-space(/)
These are all absolute expressions that do not depend on the context node. There are many more depending on what the context node is. For example, if the context node were set to the methodResponse document element, then these relative location paths and function calls would also work:
params/param/value/double
child::params/child::param/child::value/child::double[1]
.//double
normalize-space(.//double[1])
normalize-space(params)
normalize-space(/)
Assuming the relevant server response has already been parsed into a DOM Document object named response, the following code will extract the desired element into an XPathResult object:
Document response; // Initialize response object by parsing request... String query = "/methodResponse/params/param/value/double"; if (impl.hasFeature("XPath", "3.0")) { XPathEvaluator evaluator = (XPathEvaluator) response; try { XPathResult index = evaluator.evaluate(query, response, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null) // work with the result... } catch (XPathException e) { System.err.println(query + " is not a correct XPath expression"); } catch (DOMException e) { System.err.println(e); } }
What this builds is an XPathResult object, which is one step removed from the string you actually want. The XPathResult interface is a wrapper for the four things an XPath expression might evaluate to (double, string, boolean, or node-set). Getter methods are provided to return the relevant type from the XPathResult. Example 16.7 shows this interface.
Example 16.7. The XPathResult interface
package org.w3c.dom.xpath; public interface XPathResult { public static final short ANY_TYPE = 0; public static final short NUMBER_TYPE = 1; public static final short STRING_TYPE = 2; public static final short BOOLEAN_TYPE = 3; public static final short UNORDERED_NODE_ITERATOR_TYPE = 4; public static final short ORDERED_NODE_ITERATOR_TYPE = 5; public static final short UNORDERED_NODE_SNAPSHOT_TYPE = 6; public static final short ORDERED_NODE_SNAPSHOT_TYPE = 7; public static final short ANY_UNORDERED_NODE_TYPE = 8; public static final short FIRST_ORDERED_NODE_TYPE = 9; public short getResultType(); public double getNumberValue() throws XPathException; public String getStringValue() throws XPathException; public boolean getBooleanValue() throws XPathException; public Node getSingleNodeValue() throws XPathException; public boolean getInvalidIteratorState(); public int getSnapshotLength() throws XPathException; public Node iterateNext() throws XPathException, DOMException; public Node snapshotItem(int index) throws XPathException; }
Of the four getXXXValue() methods, only one of them will actually return a sensible result for any given XPath expression. The other three will throw an XPathException with the error code XPathException.TYPE_ERR. The above example expected only a single node as a result of evaluating the XPath location path /methodResponse/params/param/value/double. Consequently, the getSingleNodeValue() method can retrieve it:
Element doubleNode = (Element) index.getSingleNodeValue();
That this expression returns a single value is indicated by foreknowledge of the input format, not by anything intrinsic to the XPath expression. If there was more than one double element in the client request, then the location path would find them all.
Now we have an Element node, but what we really need is the complete text of that node, after accounting for possible if unlikely comments, CDATA sections, processing instructions, and other detritus that DOM presents us with. In Chapter 10 I developed a getFullText() utility method to account for this, and I could use it again here. However, DOM XPath offers a simpler solution. The getStringValue() method returns the XPath value of the node-set. The XPath value of an element node is defined as the complete text of the node after all character references, entity references, and CDATA sections are resolved and all other markup is stripped. Thus instead of requesting a Node, you can ask for a String:
String value = index.getStringValue();
Or maybe it’s not a String you want but a number. In this case, use getNumberValue() which returns a double:
double value = index.getNumberValue();
The DOM 3 XPath methods getStringValue(), getNumberValue(), and getBooleanValue() correspond to the XPath casting functions string(), number(), and boolean(). XPath has a number of other useful functions. For instance, normalize-space() first converts its argument to a string as if by the string() function and then strips all leading and trailing white space and converts all other runs of whitespace to a single space. Using this function, you can use a simpler location path:
XPathResult index = evaluator.evaluate("normalize-space(/)", response, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null) String value = index.getStringValue();
In the case of an XPath expression that evaluates to a node-set, getSingleNodeValue() only returns the first node in the set. You can invoke iterateNext() and then call getSingleNodeValue() again to get the second node in the set. Repeat this procedure for the third node, the fourth node, and so on until getSingleNodeValue() returns null, indicating that there are no more nodes in the set. If the set is empty to begin with, then getSingleNodeValue() returns null immediately. This is how you’d handle a case like the SOAP response that returns multiple int elements.
Because the SOAP request document uses namespace qualified elements, however, we’ll first have to provide some namespace bindings that can be used when evaluating the XPath expression. The XPathNSResolver interface provides the namespace bindings. Although you can implement this in any convenient class, an instance is normally created by passing a Node with all necessary bindings to the createNSResolver() method of the XPathEvaluator interface. For example, this code uses JAXP to build a very simple document whose document element binds the prefix SOAP to the URI http://schemas.xmlsoap.org/soap/envelope/ and the prefix f to the URI http://namespaces.cafeconleche.org/xmljava/ch3/. Then that document element is passed to the XPathEvaluator’s createNSResolver() method to create an XPathNSResolver object that has the same namespace bindings as the synthetic node we created.
// Load JAXP DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); // Build the document DOMImplementation impl = builder.getDOMImplementation(); Document namespaceHolder = impl.createDocument( "http://namespaces.cafeconleche.org/xmljava/ch3/", "f:namespaceMapping", null); // Attach the namespace declaration attributes Element root = namespaceHolder.getDocumentElement(); root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:SOAP", "http://schemas.xmlsoap.org/soap/envelope/"); root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:f", "http://namespaces.cafeconleche.org/xmljava/ch3/"); // Create the resolver XPathNSResolver namespaces = evaluator.createNSResolver(root);
Now we’re ready to repeat the earlier example, but this time using the DOM XPath API instead of the processor specific Xalan or Saxon APIs. To relieve the tedium, I’m going to make a small shift in the pattern of the readResponse() method. Instead of storing the XPath search string and the namespace bindings in the source code, I’m going to split them out into the separate XML document shown in Example 16.8 that can be bundled with the application and is assumed to live at the relative URL config.xml. (A more realistic example could store this document as a resource in the application’s JAR archive.)
Example 16.8. An XML document containing namespace bindings and an XPath search expression
<?xml version="1.0"?> <config search="/SOAP:Envelope/SOAP:Body/f:Fibonacci_Numbers/f:fibonacci" xmlns:f="http://namespaces.cafeconleche.org/xmljava/ch3/" xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" />
The program both reads the XPath expression from search attribute of the document element and uses that element for the namespace bindings. This enables the XPath string to change independently of the source code.
Here’s the configurable, DOM-XPath based readResponse() method. Since the iterator always returns a DOM node, we have to use a second XPath evaluation on each node to take the element node’s string value.
public static void readResponse(InputStream in) throws IOException, SAXException, DOMException, XPathException, ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); // Parse the server response InputSource data = new InputSource(in); Node doc = builder.parse(data); // Check to see that XPath is supported if (!impl.hasFeature("XPath", "3.0")) { throw new XPathException( "Implementation does not support XPath"); } XPathEvaluator evaluator = (XPathEvaluator) doc; // Parse the config file Document config = builder.parse("config.xml"); Element root = config.getDocumentElement(); String query = root.getAttributeValue("search"); XPathNSResolver namespaces = evaluator.createNSResolver(root); // Evaluate the expression XPathResult nodes = evaluator.evaluate( query, doc, namespaces, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); // work with the result... Node next; while (next = nodes.iterateNext()) { XPathResult stringValue = evaluator.evaluate("string()", next, namespaces, XPathResult.STRING_TYPE, null); System.out.println(stringValue.getStringValue()); } }
Iterators like this one are only good for a single pass. You cannot reuse them or back up in them. Furthermore, if the Document object over which the iterator is traversing changes before you're finished with the iterator (e.g. a node in the iterator is deleted from the Document object) then iterateNext() throws a DOMException with the code INVALID_STATE_ERR.
To hold on to a more stable list that can be reused and survives document edits, request a snapshot of the node-set returned rather than an iterator. A snapshot is reusable and features random access through indexing. For example, using a snapshot the above code would become:
// Evaluate the expression XPathResult nodes = evaluator.evaluate( "/SOAP:Envelope/SOAP:Body/f:Fibonacci_Numbers/f:fibonacci", doc, namespaces, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (int i = 0; i < nodes.getSnapshotLength(); i++) { Node next = nodes.snapshotItem(i); XPathResult stringValue = evaluator.evaluate("string()", next, namespaces, XPathResult.STRING_TYPE, null); System.out.println(stringValue.getStringValue()); } }
Of course, snapshots have the opposite problem: there is no guarantee that the nodes in the snapshot reflect the current state of the Document.
An XPath engine that implements the DOM XPath API may need to compile the expression into some internal form rather than simply keeping it as a generic String. The XPathExpression interface, shown in Example 16.9, represents such a compiled expression.
Example 16.9. The DOM3 XPathExpression interface
package org.w3c.dom.xpath; public interface XPathExpression { public XPathResult evaluate(Node contextNode, short type, XPathResult result) throws XPathException, DOMException; }
You can use the createExpression() method in the XPathEvaluator interface to compile a String into an XPathExpression:
public XPathExpression createExpression(String expression, XPathNSResolver resolver)
throws XPathException;
Then you can repeatedly invoke the same expression on different documents without having to compile the XPath expression from a string each time. For example,
XPathExpression expression = evaluator.createExpression( "/SOAP:Envelope/SOAP:Body/f:Fibonacci_Numbers/f:fibonacci", namespaces); XPathResult nodes = expression.evaluate(doc, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
This isn’t very important for an expression that’s only going to be used once or twice. However, in a program that will process many documents in the same way, it can be a significant savings. Imagine, for example, an XML-RPC or SOAP server that receives thousands of requests an hour and needs to apply the same XPath expression to each request document. The exact amount of speed you’ll gain by compiling your expressions will of course vary from implementation to implementation.
Copyright 2001, 2002 Elliotte Rusty Harold | elharo@metalab.unc.edu | Last Modified June 04, 2002 |
Up To Cafe con Leche |