60
votes

I'm trying to do a xpath lookup on nodes returned by xpath lookup, but it doesn't seem to work as I expected.XPaths executed on the child nodes of a document seem to be executd against hthe root node of the document (in the example, the inventory tag.), instead of the root of the provided node.

Am I missing something here? I'm new to XPath.

Also, please don't answer "just do //book[author='Neal Stephenson'/title". I have a legitimate use case, and this is a simplified example.

Code snippet

DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("src/main/java/books.xml");

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();

Node book = (Node) xpath.evaluate("//book[author='Neal Stephenson']", doc, XPathConstants.NODE);
Node title = (Node) xpath.evaluate("/title", book, XPathConstants.NODE); // I get null here.
Node inventory = (Node) xpath.evaluate("/inventory", book, XPathConstants.NODE); // this returns a node.

book.xml

<inventory>
<book year="2000">
    <title>Snow Crash</title>
    <author>Neal Stephenson</author>
    <publisher>Spectra</publisher>
    <isbn>0553380958</isbn>
    <price>14.95</price>
</book>

<book year="2005">
    <title>Burning Tower</title>
    <author>Larry Niven</author>
    <author>Jerry Pournelle</author>
    <publisher>Pocket</publisher>
    <isbn>0743416910</isbn>
    <price>5.99</price>
</book>

<book year="1995">
    <title>Zodiac</title>
    <author>Neal Stephenson</author>
    <publisher>Spectra</publisher>
    <isbn>0553573862</isbn>
    <price>7.50</price>
</book>

<!-- more books... -->

</inventory>
4
Others have provided good answers to your question. Note that //book and other expressions beginning with // may affect performance, as the entire DOM tree must be searched. /inventory/book is more efficient. On the same note, if an XPath expression is going to be used multiple times, you should probably compile it once with XPath.compile() and run XPathExpression.evaluate() on the result, instead of repeatedly calling XPath.evaluate().markusk
It works this way not only in Java. .NET does the same thing.ajeh

4 Answers

57
votes

/foo will select based off of the root node, ignoring the context that you are evaluating the xpath against. foo (without the slash) is what you want; that selects based off of the current node.

https://www.w3schools.com/xml/xpath_syntax.asp gives a bit more info.

28
votes

in Xpath, "." (Dot) represents the current document. So, write your XPATH string after a "." (Dot) .

ex :

"./title"

or

".//title"

Whatever you want....

removing the slash works only if its a child of the node. What if you want to use the // (wherever in the current Document) functionality ?

So, use the dot (.)

Thanks a lot for the above answers too :) .

8
votes

Just take the leading slash off of your subqueries and you should be fine. So you get your books via "//book", and then from there it's just "title", "inventory" etc to get the child bits.

5
votes

What is actually weird in the Java implementation is that a Node extracted from a Document still references the parent Document (see Node.getOwnerDocument()) and xpath uses this to find the root.

Others have mentioned a way to modify the xpath to actually not start from the root by removing the slashes.

I had a similar issue but I wanted the xpath to handle both root documents and child nodes (with an xpath like /title). The solution was to clone the node : Node.cloneNode(true). Note the true parameter that makes the Node shake its parent Document off.

...In the end, it hurts performance too much and having separate xpaths to handle Node and Document was preferred.