suppose I have an XML like this:
<body> <nested attr="bla"> <name>foo</name> </nested> <nested attr="blub"> <name>bar</name> </nested> <nested attr="bli"> <name>baz</name> </nested> </body>
What I want now is a way to get the index of the nested
tag with a certain value. So for example the index of bar
would be 1 (or 2 if you count from 1).
I have already done this using Apache CachedXPathApi:
public int getIndex(String path, String value) throws TransformerException { NodeIterator it = cachedXPathAPI.selectNodeIterator(document, path); Node node; int i = 0; while((node = it.nextNode()) != null) { if(node.getTextContent().equals(value)) { return i; } ++i; } return -1; }
Which works fine for getting the index from the attribute attr
like this:
getIndex("/body/nested/@attr", "blub")
But I don’t know how to do this for the nested values. If I use /body/nested/name
then it will obviously only ever count the name
tags within nested
which is not what I want.
How can I solve this, either by changing the Java Code or maybe even with a special XPath expression?
Advertisement
Answer
Just evaluate this XPath expression:
count(/*/*[name='bar']/preceding-sibling::*) + 1
XSLT-based verification:
The following transformation simply evaluates the XPath expression and outputs the result of this evaluation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output omit-xml-declaration="yes" indent="yes"/> <xsl:template match="/"> <xsl:value-of select="count(/*/*[name='bar']/preceding-sibling::*) +1"/> </xsl:template> </xsl:stylesheet>
When applied on the provided XML document:
<body> <nested attr="bla"> <name>foo</name> </nested> <nested attr="blub"> <name>bar</name> </nested> <nested attr="bli"> <name>baz</name> </nested> </body>
the wanted, correct result is produced:
2
II. Update
In a comment the OP described another case, where there is no element in the XML document that satisfies the filtering condition. In such case the previous expression (above) returns 1
and this is incorrect.
Here is an XPath expression that returns the correct result in all cases:
(boolean(/*/*[name='bar'])) * (count(/*/*[name='bar']/preceding-sibling::*) +1)
This is the previous XPath expression, multiplied by another XPath expression (on its left) which evaluates to 0
if there is no element that satisfies the filtering predicate, and to 1
otherwise.
The XSLT-based verification shows that this XPath expression evaluates to the correct 1-based index values in the case when a satisfying element exists, and to 0
when no such element exists. Here we take advantage of the fact that the implicit conversion number(false())
is 0
and number(true())
is 1
.
Here is the latter example:
<body> <nested attr="bla"> <name>foo</name> </nested> <nested attr="blub"> <name>barr</name> </nested> <nested attr="bli"> <name>baz</name> </nested> </body>
Here no element has a <name>
child with string value 'bar'
. And when we apply the transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output omit-xml-declaration="yes" indent="yes"/> <xsl:template match="/"> <xsl:value-of select= "(boolean(/*/*[name='bar'])) * (count(/*/*[name='bar']/preceding-sibling::*) +1)"/> </xsl:template> </xsl:stylesheet>
the correct, wanted result is produced:
0