Combining elements from 2 lists in XSLT
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.
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.
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:
Using and overriding the identity rule is a fundamental XSLT design pattern and its use facilitates the writing of almost any transformation.
Using
<xsl:apply-templates/>
instead of<xsl:apply-templates select="node()[1]"/>
allows parallel execution, which may become increasingly important in the nearest future.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.
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.
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.
精彩评论