1

Lets say I have XML that looks like so (and assume I can't alter this XML's format):

<biscuit>
 <name>Hobnobs</name>
 <price>1.49</price>
 <name>Digestives</name>
 <price>89.00</price>
</biscuit>


 <biscuitInfo name="Hobnobs">
  <nutritionalValue>
     <fat>6 grams</fat>
     <sugar>lots</sugar>
   </nutritionalValue>      
  </biscuitInfo>
  <biscuitInfo name="Digestives">
   <nutritionalValue>
     <fat>3 grams</fat>
     <sugar>5 grams</sugar>
   </nutritionalValue>
 </biscuitInfo>

And I would like to use XSLT to turn this into something that looks like so:

<biscuit>
     <name>Hobnobs</name>
     <price>1.49</price>
     <fat>6 grams</fat>
     <sugar>lots</sugar>
</biscuit>

How would I go about doing something like this in XSLT? Can I loop through the first list of biscuits (name & price) and pull in elements from the second list (nutritional values)?

I'm not too savvy with XSL, so any advice would be welcome.

Cheers,

JD.

jdoig
  • 1,472
  • 13
  • 27

4 Answers4

2

Two examples. This stylesheet using clasic full recursion:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="kNutChildByBisName" match="nutritionalValue/*"
             use="../../@name"/>
    <xsl:key name="kElemByPrecedingName" match="biscuit/*[not(self::name)]"
             use="preceding-sibling::name[1]"/>
    <xsl:template match="node()|@*" name="identity">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="name" mode="group">
        <bisquit>
            <xsl:apply-templates select=".|key('kNutChildByBisName',.)|
                                         key('kElemByPrecedingName',.)"/>
        </bisquit>
    </xsl:template>
    <xsl:template match="biscuit">
        <xsl:apply-templates mode="group"/>
    </xsl:template>
    <xsl:template match="biscuitInfo"/>
    <xsl:template match="node()" mode="group"/>
</xsl:stylesheet>

And this stylesheet using fine grained traversal:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:key name="kNutByBisName" match="nutritionalValue"
                 use="../@name"/>
    <xsl:template match="node()|@*" name="identity">
        <xsl:copy>
            <xsl:apply-templates select="node()[1]|@*"/>
        </xsl:copy>
        <xsl:apply-templates select="following-sibling::node()[1]"/>
    </xsl:template>
    <xsl:template match="biscuitInfo"/>
    <xsl:template match="biscuit">
        <xsl:apply-templates select="node()[1]|following-sibling::node()[1]"/>
    </xsl:template>
    <xsl:template match="name[1]" name="group">
        <bisquit>
            <xsl:call-template name="identity"/>
            <xsl:apply-templates select="key('kNutByBisName',.)/node()[1]"/>
        </bisquit>
        <xsl:apply-templates select="following-sibling::name[1]" mode="group"/>
    </xsl:template>
    <xsl:template match="name"/>
    <xsl:template match="name" mode="group">
        <xsl:call-template name="group"/>
    </xsl:template>
</xsl:stylesheet>

With this input:

<root>
    <biscuit>
        <name>Hobnobs</name>
        <price>1.49</price>
        <name>Digestives</name>
        <price>89.00</price>
    </biscuit>
    <biscuitInfo name="Hobnobs">
        <nutritionalValue>
            <fat>6 grams</fat>
            <sugar>lots</sugar>
        </nutritionalValue>
    </biscuitInfo>
    <biscuitInfo name="Digestives">
        <nutritionalValue>
            <fat>3 grams</fat>
            <sugar>5 grams</sugar>
        </nutritionalValue>
    </biscuitInfo>
</root>

Both output:

<root>
    <bisquit>
        <name>Hobnobs</name>
        <price>1.49</price>
        <fat>6 grams</fat>
        <sugar>lots</sugar>
    </bisquit>
    <bisquit>
        <name>Digestives</name>
        <price>89.00</price>
        <fat>3 grams</fat>
        <sugar>5 grams</sugar>
    </bisquit>
</root>

Note: You are performing two task: grouping and cross referencing.

Edit: Better fine grained traversal in case there is only name in group.

  • No offence here, but I'm really interested, why to use identity transorm in cases, wheret it makes more code and is, imho, less readable? – Flack Dec 13 '10 at 17:21
  • 1
    @Flack: Reuse and semantic meaning: grouping is just grouping with `apply-templates` instead of `copy-of`, then you could transform those elements too. Also, the only assumptions are: `bisquit` children are grouped, group start with `name`. Besides that, the schema could grow without changing the stylesheet. –  Dec 13 '10 at 17:39
  • @Alejandro +1 Thanks. I've got your point. May be it's just my bad habit due to using xml-html transformations 90% of time. – Flack Dec 13 '10 at 17:47
1

What you could do is use XPath for the second part. Do the loop on your first list, and as soon as you have the name, use XPath to query the exact thing you want in the second list.

Valentin Rocher
  • 11,667
  • 45
  • 59
  • Would that require the creation of a variable in xslt that represents the value the loop is currently focused on? or can I just use it directly in the Xpath query? – jdoig Dec 13 '10 at 10:35
  • 1
    I think both options can be valid. Creating a variable would give you shorter expressions, but I think you can directly use your value in the xpath query. – Valentin Rocher Dec 13 '10 at 10:49
  • That's great, thank you. I don't suppose you have any good links showing such a technique do you? Just to save me struggling. Cheers. – jdoig Dec 13 '10 at 10:52
1

This XSLT:

<?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" indent="yes"/>

<xsl:key name="bisquit-info" match="/root/biscuitInfo" use="@name"/>

<xsl:template match="root">
    <xsl:copy>
        <xsl:apply-templates select="biscuit/name"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="name">
    <bisquit>
        <xsl:copy-of select="."/>
        <xsl:copy-of select="following-sibling::price[1]"/>
        <xsl:copy-of select="key('bisquit-info', .)/nutritionalValue/*"/>
    </bisquit>
</xsl:template> 
</xsl:stylesheet>

With such XML input (just added root node for wellformedness)

<root>
<biscuit>
    <name>Hobnobs</name>
    <price>1.49</price>
    <name>Digestives</name>
    <price>89.00</price>
</biscuit>

<biscuitInfo name="Hobnobs">
    <nutritionalValue>
    <fat>6 grams</fat>
    <sugar>lots</sugar>
    </nutritionalValue>      
</biscuitInfo>
<biscuitInfo name="Digestives">
    <nutritionalValue>
    <fat>3 grams</fat>
    <sugar>5 grams</sugar>
    </nutritionalValue>
</biscuitInfo>
</root>

Will provide this result:

<root>
<bisquit>
    <name>Hobnobs</name>
    <price>1.49</price>
    <fat>6 grams</fat>
    <sugar>lots</sugar>
</bisquit>
<bisquit>
    <name>Digestives</name>
    <price>89.00</price>
    <fat>3 grams</fat>
    <sugar>5 grams</sugar>
</bisquit>
</root>

The design is not ideal, but this would work. I used keys, assuming that input tree can be large.

Flack
  • 5,862
  • 2
  • 23
  • 27
1

A traditional identity rule override by a few templates. No keys, no fine-grain traversal:

<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:template match="node()|@*" name="identity">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="biscuit/name">
  <biscuit>
    <xsl:call-template name="identity"/>
    <xsl:apply-templates select=
    "following-sibling::price[1]
    |
      ../../biscuitInfo[@name = current()]/*"/>
  </biscuit>
 </xsl:template>

 <xsl:template match="biscuit">
  <xsl:apply-templates select="name"/>
 </xsl:template>

 <xsl:template match="nutritionalValue">
   <xsl:apply-templates/>
 </xsl:template>
 <xsl:template match="biscuitInfo"/>
</xsl:stylesheet>

When applied on this source XML (essentially the provided one, but wrapped in a single top element):

<product>
    <biscuit>
        <name>Hobnobs</name>
        <price>1.49</price>
        <name>Digestives</name>
        <price>89.00</price>
    </biscuit>
    <biscuitInfo name="Hobnobs">
        <nutritionalValue>
            <fat>6 grams</fat>
            <sugar>lots</sugar>
        </nutritionalValue>
    </biscuitInfo>
    <biscuitInfo name="Digestives">
        <nutritionalValue>
            <fat>3 grams</fat>
            <sugar>5 grams</sugar>
        </nutritionalValue>
    </biscuitInfo>
</product>

the wanted, correct result is produced:

<product>
   <biscuit>
      <name>Hobnobs</name>
      <price>1.49</price>
      <fat>6 grams</fat>
      <sugar>lots</sugar>
   </biscuit>
   <biscuit>
      <name>Digestives</name>
      <price>89.00</price>
      <fat>3 grams</fat>
      <sugar>5 grams</sugar>
   </biscuit>
</product>

Do note:

  1. Using and overriding the identity rule is a fundamental XSLT design pattern and its use facilitates the writing of almost any transformation.

  2. Using <xsl:apply-templates/> instead of <xsl:apply-templates select="node()[1]"/> allows parallel execution, which may become increasingly important in the nearest future.

  3. Keys could be used as an optimization, but this isn't necessary for small XML documents like the provided one and the usage of keys isn't related to the central idea in the solution of this problem.

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431