Xml – Problem using ancestor axis in an XPath predicate

xmlxpathxslt

Using this source document:

<?xml version="1.0" encoding="UTF-8"?>
<Root>
  <Element1 id="UniqueId1">
    <SubElement1/>
    <SubElement2>
      <LeafElement1/>
      <LeafElement1/>
    </SubElement2>
  </Element1>
  <Element2 id="UniqueId2" AttributeToCheck="true">
    <SubElement1>
      <LeafElement1/>
      <LeafElement1/>
    </SubElement1>
  </Element2> 
</Root>

I want to add the attribute foo="bar" to elements that both:

  1. Have sibling elements with the same name
  2. Have any ancestor with attribute AttributeToCheck

This should be the result:

<?xml version="1.0"?>
<Root>
  <Element1 id="UniqueId1">
    <SubElement1/>
    <SubElement2>
      <LeafElement1/>
      <LeafElement1/>
    </SubElement2>
  </Element1>
  <Element2 id="UniqueId2" AttributeToCheck="true">
    <SubElement1>
      <LeafElement1 foo="bar"/>
      <LeafElement1 foo="bar"/>
    </SubElement1>
  </Element2>
</Root>

This is my stlesheet so far. It adds the attribute elements matching condition 1 but fails to account for condition 2.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" indent="yes"/>

  <xsl:template match="* | @*">
    <xsl:copy>
      <xsl:apply-templates select="* | @*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="*[count(../*[name(.) = name(current())]) > 1]">
    <xsl:copy>
      <xsl:attribute name="foo">bar</xsl:attribute>
      <xsl:apply-templates select="* | @*"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

The incorrect output:

<?xml version="1.0"?>
<Root>
  <Element1 id="UniqueId1">
    <SubElement1/>
    <SubElement2>
      <LeafElement1 foo="bar"/> (incorrect)
      <LeafElement1 foo="bar"/> (incorrect)
    </SubElement2>
  </Element1>
  <Element2 id="UniqueId2" AttributeToCheck="true">
    <SubElement1>
      <LeafElement1 foo="bar"/> (correct)
      <LeafElement1 foo="bar"/> (correct)
    </SubElement1>
  </Element2>
</Root>

Since the second template already correctly matches elements that have siblings with the same name, it should be easy to use the ancestor XPath axis to exclude elements without AttributeToCheck ancestors. I added another predicate to the second template.

<xsl:template match="*[ancestor::*[@AttributeToCheck]][count(../*[name(.) = name(current())]) > 1]">

When I apply this stylesheet, the output document is the same as the input document, showing that the second template doesn't match any elements. I also tried changing the new predicate to use the node count.

<xsl:template match="*[count(ancestor::*[@AttributeToCheck]) > 0][count(../*[name(.) = name(current())]) > 1]">

This also didn't work, the output document was the same as the input document. This is surprising, because when I use this ancestor expression to output the name of nodes with AttributeToCheck it works. I made these changes to the sylesheet:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" indent="yes"/>

  <xsl:template match="* | @*">
    <xsl:copy>
      <xsl:apply-templates select="* | @*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="*[count(../*[name(.) = name(current())]) > 1]">
    <xsl:copy>
      <xsl:attribute name="foo">bar</xsl:attribute>
      <xsl:attribute name="AncestorCount">
        <xsl:value-of select="count(ancestor::*[@AttributeToCheck])"/>
      </xsl:attribute>
      <xsl:attribute name="AncestorName">
        <xsl:value-of select="name(ancestor::*[@AttributeToCheck])"/>
      </xsl:attribute>
      <xsl:apply-templates select="* | @*"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Produces this output:

<?xml version="1.0"?>
<Root>
  <Element1 id="UniqueId1">
    <SubElement1/>
    <SubElement2>
      <LeafElement1 foo="bar" AncestorCount="0" AncestorName=""/>
      <LeafElement1 foo="bar" AncestorCount="0" AncestorName=""/>
    </SubElement2>
  </Element1>
  <Element2 id="UniqueId2" AttributeToCheck="true">
    <SubElement1>
      <LeafElement1 foo="bar" AncestorCount="1" AncestorName="Element2"/>
      <LeafElement1 foo="bar" AncestorCount="1" AncestorName="Element2"/>
    </SubElement1>
  </Element2>
</Root>

My question is, why does the XPath predicate *[ancestor::*[@AttributeToCheck]][count(../*[name(.) = name(current())]) > 1] not correctly match elements matching both conditions 1 and 2? What XPath expression should I use instead?

Best Answer

I think this is a problem on XSL processor rather than your XSLT. (Except for count(ancestor::*[@AttributeToCheck]) > 0 should be such as ancestor::*[@AttributeToCheck='true'], as you might notice.)

I tried the following and it seems to work correctly on XmlStarlet:

<xsl:template match="*[ancestor::*[@AttributeToCheck='true'] and count(../*[name(.)=name(current())]) > 1]">

On my interpretation of §2.1 and §3.4 of XPath 1.0 spec, your expression and one above have same effect (at least) in this case.

Another solution is to use templates with mode. This can be useful if @AttributeToCheck can be nested.

Related Topic