开发者

Problem using ancestor axis in an XPath predicate

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?


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">
  <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>


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.


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>
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜