tags:

views:

629

answers:

3

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"&gt;
  <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"&gt;
  <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?

+1  A: 

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.

habe
This does look to be due to an error in XmlStarlet. habe's XPath expression is functionaly equivalent to mine, yet his works and mine doesn't.
Chris Stevens
+3  A: 

I would use only one template (pattern matching is costly):

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

  <xsl:template match="* | @*">
    <xsl:copy>
      <xsl:if test="count(../*[name(current()) = name()]) > 1 and ancestor::*[@AttributeToCheck='true']">
        <xsl:attribute name="foo">bar</xsl:attribute>      
      </xsl:if>
      <xsl:apply-templates select="* | @*"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>
Erlock
+1  A: 

You don't need to use count(), since the empty node-set evaluates to false. Simply selecting the nodes you are interested in is sufficient as a condition:

<xsl:stylesheet 
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
  <!-- modified identity transform -->
  <xsl:template match="node() | @*">
    <xsl:copy>
      <!-- check both your conditions -->
      <xsl:if test="
        ancestor::*[@AttributeToCheck = 'true']
        and
        (preceding-sibling::* | following-sibling::*)[name() = name(current())]
      ">
        <xsl:attribute name="foo">bar</xsl:attribute>
      </xsl:if>
      <xsl:apply-templates select="node() | @*" />
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
Tomalak
mmh, I'm not sure that your answer complies with the rule "Have sibling elements with the same name". ../*[name() = name(current())] will return only one node, the current element, if it's the only one of his kind.
Erlock
@Erlock: You are absolutely right. Stupid mistake, I'll correct it.
Tomalak