Using XSLT Apply-Templates to conditionally select nodes

37,897

Solution 1

The condition should be on xsl:apply-templates instead of xsl:template:

<Plays>
  <xsl:apply-templates select="play[a='Will Smith']">"/>
</Plays>

In your solution, you are transforming ALL <play> nodes. For play nodes that match the condition, your template is applied. But for those that don't match the condition, a default template ("identity transform") is applied instead.

Alternatively, you could keep the condition on xsl:template match, but add another template for <play> that do not match the condition, to transform those <play> into nothing:

    <xsl:template match="play[a='Will Smith']">
      <play title="{data(t)[1]}">
        <xsl:apply-templates select="a"/>
      </play>
    </xsl:template>

    <xsl:template match="play">
    </xsl:template>

Solution 2

I. Probably the most efficient XSLT 1.0 solution:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:key name="kWSPlayByTitle" match="play[a='Will Smith']"
  use="t"/>

 <xsl:key name="kActorByTitle" match="a"
  use="../t"/>

 <xsl:template match="/">
  <Plays>
    <xsl:apply-templates select=
    "*/play[generate-id()
           =
            generate-id(key('kWSPlayByTitle',t)[1])
           ]"/>
  </Plays>
 </xsl:template>

 <xsl:template match="play">
  <Play title="{t}">
   <xsl:apply-templates select="key('kActorByTitle',t)"/>
  </Play>
 </xsl:template>

 <xsl:template match="a">
  <star><xsl:value-of select="."/></star>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the provided XML document:

<director>
    <play>
        <t>Nutcracker</t>
        <a>Tom Cruise</a>
    </play>
    <play>
        <t>Nutcracker</t>
        <a>Robin Williams</a>
    </play>
    <play>
        <t>Grinch Stole Christmas</t>
        <a>Will Smith</a>
    </play>
    <play>
        <t>Grinch Stole Christmas</t>
        <a>Mel Gibson</a>
    </play>
</director>

the wanted result is produced:

<Plays>
   <Play title="Grinch Stole Christmas">
      <star>Will Smith</star>
      <star>Mel Gibson</star>
   </Play>
</Plays>

Do note:

  1. Efficiency is achieved by using keys both for all plays in which Mell Gibson took part and for all actors that took part in a given (titled) play.

  2. Even if a play title with Mel Gibson were listed more than once (due to accidental error, perhaps...) it will be listed only once in the result.

II. A simple and efficient XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
  <Plays>
    <xsl:for-each-group select="play[a='Mel Gibson']"
          group-by="t">
      <xsl:apply-templates select="."/>
    </xsl:for-each-group>
  </Plays>
 </xsl:template>

 <xsl:template match="play">
  <Play title="{t}">
   <xsl:for-each-group select="../play[t = current()/t]/a"
        group-by=".">
     <xsl:apply-templates select="."/>
   </xsl:for-each-group>
  </Play>
 </xsl:template>

 <xsl:template match="a">
  <star>
    <xsl:value-of select="."/>
  </star>
 </xsl:template>
</xsl:stylesheet>
Share:
37,897
Msencenb
Author by

Msencenb

Updated on August 11, 2022

Comments

  • Msencenb
    Msencenb almost 2 years

    Let's say I have an xml document like this:

    <director>
        <play>
            <t>Nutcracker</t>
            <a>Tom Cruise</a>
        </play>
        <play>
            <t>Nutcracker</t>
            <a>Robin Williams</a>
        </play>
        <play>
            <t>Grinch Stole Christmas</t>
            <a>Will Smith</a>
        </play>
        <play>
            <t>Grinch Stole Christmas</t>
            <a>Mel Gibson</a>
        </play>
    </director>
    

    Now I want to be able to select all the plays with Will Smith as an actor and reformat it into something like this:

    <Plays>
        <Play title="Grinch Stole Christmas">
           <star>Will Smith</star>
           <star>Mel Gibson</star>
        </Play>
    </Plays>
    

    I only want to use apply-templates.. No xsl:if or for each loops (I have contrived this example as a simpler version of what I'm doing so you can help me understand how to use xpath within a match statement)

    Here is what I have so far:

    <?xml version="1.0"?>
    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" />
            <xsl:template match="/director">
                    <Plays>
                    <xsl:apply-templates select="play"/>
                    </Plays>
            </xsl:template>
    
            <xsl:template match="play[a='Will Smith']">
                    <play title="{data(t)[1]}">
                    <xsl:apply-templates select="a"/>
                    </play>
            </xsl:template>
    
            <xsl:template match="a">
                    <star>
                    <xsl:value-of select="."/>
                    </star>
            </xsl:template>
    </xsl:stylesheet>
    

    Basically I am just unsure of how to filter out nodes using XPath in the match attribute of the template. Any help would be great!