tags:

views:

44

answers:

3

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

+1  A: 

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>
ckarras
Thank you! This works like a charm now
Msencenb
@Msencenb: This doesn't output the desired result.
Alejandro
Are you sure? The very first code example given seems to work for me
Msencenb
A: 

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:key name="kActorByTitle" match="a" use="../t"/>
    <xsl:param name="pActor" select="'Will Smith'"/>
    <xsl:template match="/">
        <Plays>
            <xsl:apply-templates select="*/play[a=$pActor]"/>
        </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>

Output:

<Plays>
    <Play title="Grinch Stole Christmas">
        <star>Will Smith</star>
        <star>Mel Gibson</star>
    </Play>
</Plays>
Alejandro
This works well too and uses keys like another answer. I think the key takeaway for me is that the conditions should be within the select rather than the match statement
Msencenb
@Msencenb: Pattern matching driven vs. select driven is a matter of pull vs push style. Both are ok. In this case, because I use a parameter in a XSLT 1.0 stylesheet, I had to go with a push style (param/var references aren't allowed in XSLT 1.0 patterns)
Alejandro
@Alejandro, @@Msencenb: Variable/parameter references are allowed in the match pattern in XSLT 2.0
Dimitre Novatchev
A: 

I. Probably the most efficient XSLT 1.0 solution:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <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"&gt;
 <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>
Dimitre Novatchev
I had never even heard of keys... but this does appear to be the most efficient. Thanks
Msencenb
@Msencenb: Note that the XSLT 2.0 solution doesn't use keys at all. Their role is played by `<xsl:for-each-group>`. But of course, even in XSLT 2.0 it doesn't hurt, and indeed may be useful, to know how to use keys.
Dimitre Novatchev
@Dimitre: I didn't go with two keys because this expression `"*/play[generate-id()...]` will iterate over all the `play` elements anyway.
Alejandro
@Alejandro: Maybe you are right. However, if there is additional processing in which this key could be useful, then you clearly win. Maybe in future versions of XSLT (3+) we would have the possibilities to inspect all items in a given built key -- then such additional traversal will not be necessary.
Dimitre Novatchev
@Dimitre: Yes! I saw that discusion in XSLT List. Do you think Dr. Kay will make this proposal? Do you know if this is already done?
Alejandro
@Dimitre: In this case, you could have this reverse key: title by actor. But I thought it would be to much optimization...
Alejandro