Find index of nested XML tag

Tags: ,



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?

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



Source: stackoverflow