How to define attribute name dynamically in XSLT? - xslt

I have this XML:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
<xsl:param name="navigation-xml">
<item id="home" title-en="Services" title-de="Leistungen" />
<item id="company" title-en="Company" title-de="Unternehmen" />
<item id="references" title-en="References" title-de="Referenzen" />
</xsl:param>
<xsl:param name="navigation" select="exsl:node-set($navigation-xml)/*" />
<xsl:param name="navigation-id" />
<xsl:template name="title">
<xsl:apply-templates select="$navigation" mode="title" />
</xsl:template>
<xsl:template match="item" mode="title">
<xsl:if test="$navigation-id = #id">
<xsl:choose>
<xsl:when test="$current-language = 'de'">
<xsl:value-of select="#title-de" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="#title-en" />
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
How can I refactor the last 12 lines, so that the attribute name (either #title-de or #title-en) gets determined dynamically rather than in the (silly) way I did it in?
Thanks for any help.

You could write
<xsl:template name="title">
<xsl:apply-templates select="$navigation" mode="title" />
</xsl:template>
<xsl:template match="item" mode="title">
<xsl:if test="$navigation-id = #id">
<xsl:choose>
<xsl:when test="$current-language = 'de'">
<xsl:value-of select="#title-de" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="#title-en" />
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
as
<xsl:template name="title">
<xsl:apply-templates select="$navigation[$navigation-id = #id]" mode="title" />
</xsl:template>
<xsl:template match="item" mode="title">
<xsl:value-of select="#*[local-name() = concat('title-', $current-language)]" />
</xsl:template>

IMHO, your problem starts much earlier. If you define your navigation-xml parameter as:
<xsl:param name="navigation-xml">
<item id="home">
<title lang="en">Services</title>
<title lang="de">Leistungen</title>
</item>
<item id="company">
<title lang="en">Company</title>
<title lang="de">Unternehmen</title>
</item>
<item id="references">
<title lang="en">References</title>
<title lang="de">Referenzen</title>
</item>
</xsl:param>
you will be able to address its individual nodes much more conveniently and elegantly.

Related

How to call template for each dictionary elements?

This is my xml:
<A>
<D>dd</D>
<E>ee</E>
<B>
<C>1</C>
<C>2</C>
</B>
</A>
This was my xsl. I had to call template1 and template2 only few times for the same params:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="/A">
<xsl:call-template name="items1">
<xsl:with-param name="key">aa</xsl:with-param>
<xsl:with-param name="value">bb</xsl:with-param>
</xsl:call-template>
<xsl:call-template name="items2">
<xsl:with-param name="key">aa</xsl:with-param>
<xsl:with-param name="value">bb</xsl:with-param>
</xsl:call-template>
...
</xsl:template>
<xsl:template name="template1 ">
<xsl:param name="key"/>
<xsl:param name="value"/>
<xsl:value-of select="/D"/>
<xsl:value-of select="$key"/>
<xsl:value-of select="$value"/>
...
</xsl:template>
<xsl:template name="template2">
<xsl:param name="key"/>
<xsl:param name="value"/>
<xsl:value-of select="/E"/>
<xsl:value-of select="$key"/>
<xsl:value-of select="$value"/>
...
</xsl:template>
</xsl:stylesheet>
Now, I have to call my templates about 350 times for template1 and 350 times for template2, I have many key, value pairs.
I thought to make one dictionary with key value pairs and iterate over it on template1 and template2
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:variable name="values">
<item key="A" value="aa"/>
<item key="B" value="bb"/>
...
</xsl:variable>
<xsl:template match="/A">
<xsl:for-each select="$values">
<xsl:call-template name="items1">
<xsl:with-param name="key">aa</xsl:with-param>
<xsl:with-param name="value">bb</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
<xsl:for-each select="$values">
<xsl:call-template name="items2">
<xsl:with-param name="key">aa</xsl:with-param>
<xsl:with-param name="value">bb</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
...
</xsl:template>
<xsl:template name="template1 ">
<xsl:param name="key"/>
<xsl:param name="value"/>
<xsl:value-of select="/D"/>
<xsl:value-of select="$key"/>
<xsl:value-of select="$value"/>
...
</xsl:template>
<xsl:template name="template2">
<xsl:param name="key"/>
<xsl:param name="value"/>
<xsl:value-of select="/E"/>
<xsl:value-of select="$key"/>
<xsl:value-of select="$value"/>
...
</xsl:template>
</xsl:stylesheet>
My question is that Is it good idea to solve my problem. How to get root node in templates, now current node is "item". I wanted to get value od D node at my template.
It's difficult to answer your question without seeing the complete picture. Without it I can only guess you want to do:
<xsl:for-each select="$prodDict/item">
<xsl:call-template name="Item">
<xsl:with-param name="param1" select="#key"/>
<xsl:with-param name="param" select="#value"/>
</xsl:call-template>
</xsl:for-each>
Note that this requires XSLT 2.0 or higher.
Added:
How to get root node in templates, now current node is "item".
A convenient method is to place the root node in a global variable, so that it's accessible from anywhere. Here's a simplified example:
XML
<A>
<D>dd</D>
<E>ee</E>
<B>
<C>1</C>
<C>2</C>
</B>
</A>
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:variable name="values">
<item key="A" value="aa"/>
<item key="B" value="bb"/>
</xsl:variable>
<xsl:variable name="xml" select="/" />
<xsl:template match="/">
<result>
<xsl:for-each select="$values/item">
<xsl:call-template name="template1">
<xsl:with-param name="param1" select="#key"/>
<xsl:with-param name="param2" select="#value"/>
</xsl:call-template>
</xsl:for-each>
</result>
</xsl:template>
<xsl:template name="template1 ">
<xsl:param name="param1"/>
<xsl:param name="param2"/>
<item>
<d>
<xsl:value-of select="$xml/A/D"/>
</d>
<key>
<xsl:value-of select="$param1"/>
</key>
<value>
<xsl:value-of select="$param2"/>
</value>
</item>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<result>
<item>
<d>dd</d>
<key>A</key>
<value>aa</value>
</item>
<item>
<d>dd</d>
<key>B</key>
<value>bb</value>
</item>
</result>

How to rename root and children element XSLT

How do I rename root 'channel' and children 'item' to 'Records' and 'Record' respectively?
Input
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<item>
<title>A</title>
<link>/news/view/25857/A.html</link>
<guid isPermaLink="true">/news/view/25857/A.html</guid>
<comments>/news/25857/A.html</comments>
<pubDate>Sat, 03 Oct 2015 00:42:42 GMT</pubDate>
<description><![CDATA[]]></description>
<category>headline,hacker,bank,cybercrime,data loss,fraud</category>
</item>
</channel>
</rss>
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:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="item/comments">
<taglink>
<xsl:text></xsl:text>
<xsl:value-of select="text()" />
</taglink>
</xsl:template>
<!--delimits values if separated by comma-->
<xsl:template match="item/category[contains(.,',')]">
<category>
<xsl:variable name="elementName" select="name(..)"/>
<xsl:call-template name="splitIntoElements">
<xsl:with-param name="baseName" select="name(..)" />
<xsl:with-param name="txt" select="." />
</xsl:call-template>
</category>
</xsl:template>
<xsl:template name="splitIntoElements">
<xsl:param name="baseName" />
<xsl:param name="txt" />
<xsl:param name="delimiter" select="','" />
<xsl:param name="index" select="1" />
<xsl:variable name="first" select="substring-before($txt, $delimiter)" />
<xsl:variable name="remaining" select="substring-after($txt, $delimiter)" />
<xsl:element name="value">
<xsl:choose>
<xsl:when test="$first">
<xsl:value-of select="$first" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$txt" />
</xsl:otherwise>
</xsl:choose>
</xsl:element>
<xsl:if test="$remaining">
<xsl:call-template name="splitIntoElements">
<xsl:with-param name="baseName" select="$baseName" />
<xsl:with-param name="txt" select="$remaining" />
<xsl:with-param name="index" select="$index" />
<xsl:with-param name="delimiter" select="$delimiter" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Output
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<item>
<title>A</title>
<link>/news/view/25857/A.html</link>
<guid isPermaLink="true">/news/view/25857/A.html</guid>
<taglink>/news/25857/A.html</taglink>
<pubDate>Sat, 03 Oct 2015 00:42:42 GMT</pubDate>
<description/>
<category>
<value>headline</value>
<value>hacker</value>
<value>bank</value>
<value>cybercrime</value>
<value>data loss</value>
<value>fraud</value>
</category>
</item>
</channel>
</rss>
Expected Output - There are many 'item' within 'channel' so there is expected be have many 'records' within 'record'
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<Records>
<Record>
<title>A</title>
<link>/news/view/25857/A.html</link>
<guid isPermaLink="true">/news/view/25857/A.html</guid>
<taglink>/news/25857/A.html</taglink>
<pubDate>Sat, 03 Oct 2015 00:42:42 GMT</pubDate>
<description/>
<category>
<value>headline</value>
<value>hacker</value>
<value>bank</value>
<value>cybercrime</value>
<value>data loss</value>
<value>fraud</value>
</category>
</Record>
</Records>
</rss>
You can simply add a couple of templates that match element to be renamed, and spit out the new element name, for example :
<xsl:template match="channel">
<Records>
<xsl:apply-templates/>
</Records>
</xsl:template>
<xsl:template match="item">
<Record>
<xsl:apply-templates/>
</Record>
</xsl:template>

XSLT 1.0: recursively flatten/denormalize a structure

I'm trying to recursively flatten / normalize a structure below with no luck.
<models>
<model name="AAA" root="true">
<items>
<item name="a"/>
<item name="b"/>
</items>
<submodels>
<submodel ref="BBB"/>
<submodel ref="CCC" />
</submodels>
</model>
<model name="BBB">
<items>
<item name="c"/>
<item name="d"/>
</items>
<submodels>
<submodel ref="CCC" />
</submodels>
</model>
<model name="CCC">
<item name="e" />
</model>
</models>
The expected result is the following:
/AAA
/AAA/a
/AAA/b
/AAA/BBB
/AAA/BBB/c
/AAA/BBB/d
/AAA/BBB/CCC
/AAA/BBB/CCC/e
/AAA/CCC
/AAA/CCC/e
I have tried using in a recursive way. But the main issue is that several models can refer to a single model. Eg. AAA -> CCC and BBB -> CCC.
This short and simple transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kmodelByName" match="model" use="#name"/>
<xsl:key name="ksubmodelByRef" match="submodel" use="#ref"/>
<xsl:template match="/*">
<xsl:apply-templates select="model[not(key('ksubmodelByRef', #name))]"/>
</xsl:template>
<xsl:template match="model|item">
<xsl:param name="pPath"/>
<xsl:value-of select="concat('
', $pPath, '/', #name)"/>
<xsl:apply-templates select="item|*/item|*/submodel">
<xsl:with-param name="pPath" select="concat($pPath, '/', #name)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="submodel">
<xsl:param name="pPath"/>
<xsl:apply-templates select="key('kmodelByName', #ref)">
<xsl:with-param name="pPath" select="$pPath"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<models>
<model name="AAA" root="true">
<items>
<item name="a"/>
<item name="b"/>
</items>
<submodels>
<submodel ref="BBB"/>
<submodel ref="CCC" />
</submodels>
</model>
<model name="BBB">
<items>
<item name="c"/>
<item name="d"/>
</items>
<submodels>
<submodel ref="CCC" />
</submodels>
</model>
<model name="CCC">
<item name="e"/>
</model>
</models>
produces the wanted, correct result:
/AAA
/AAA/a
/AAA/b
/AAA/BBB
/AAA/BBB/c
/AAA/BBB/d
/AAA/BBB/CCC
/AAA/BBB/CCC/e
/AAA/CCC
/AAA/CCC/e
Explanation:
Proper use of keys makes the transformation short, easy to express and efficient.
Proper use of templates.
Proper use of parameter - passing to templates.
Firstly, I suspect you want to math the model that is marked as a root
<xsl:apply-templates select="model[#root='true']" />
Then you would have a template to match model elements, but one that takes a parameter that contains the current 'path' to the parent model
<xsl:template match="model">
<xsl:param name="path" />
You could output the full path like so
<xsl:variable name="newpath" select="concat($path, '/', #name)" />
<xsl:value-of select="concat($newpath, '
')" />
You could then output the items but passing in the new path as a parameter
<xsl:apply-templates select="items/item|item">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
To match the sub-models, ideally a key could be used
<xsl:key name="models" match="model" use="#name" />
Then you could match the sub-models like so
<xsl:apply-templates select="key('models', submodels/submodel/#ref)">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
This would recursively match the same model template.
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:key name="models" match="model" use="#name" />
<xsl:template match="/models">
<xsl:apply-templates select="model[#root='true']" />
</xsl:template>
<xsl:template match="model">
<xsl:param name="path" />
<xsl:variable name="newpath" select="concat($path, '/', #name)" />
<xsl:value-of select="concat($newpath, '
')" />
<xsl:apply-templates select="items/item|item">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
<xsl:apply-templates select="key('models', submodels/submodel/#ref)">
<xsl:with-param name="path" select="$newpath" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item">
<xsl:param name="path" />
<xsl:value-of select="concat($path, '/', #name, '
')" />
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML, the following is output
/AAA
/AAA/a
/AAA/b
/AAA/BBB
/AAA/BBB/c
/AAA/BBB/d
/AAA/BBB/CCC
/AAA/BBB/CCC/e
/AAA/CCC
/AAA/CCC/e
I'm not sure what your rules of transformation are. This is a bit of a guess, but the following XSLT 1.0 style-sheet does transform your supplied input to the expected output.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates select="models/model[#root='true']">
<xsl:with-param name="base" select="''" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="model|item">
<xsl:param name="base" />
<xsl:variable name="new-base" select="concat($base,'/',#name)" />
<xsl:value-of select="concat($new-base,'
')" />
<xsl:apply-templates select="items/item | item | submodels/submodel/#ref">
<xsl:with-param name="base" select="$new-base" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="#ref">
<xsl:param name="base" />
<xsl:apply-templates select="../../../../model[#name=current()]">
<xsl:with-param name="base" select="$base" />
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
There is probably an easier way to do this, but I was able to get the output you wanted with the following xslt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" exclude-result-prefixes="exsl" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common">
<xsl:variable name="root" select="/models/model[#root = 'true']/#name"/>
<xsl:template match="models">
<xsl:for-each select="model">
<xsl:variable name="savedNode" select="."/>
<xsl:variable name="precedingValue">
<xsl:call-template name="preceding">
<xsl:with-param name="modelName" select="#name"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="not(exsl:node-set($precedingValue)/preceding)">
<xsl:call-template name="model">
<xsl:with-param name="node" select="$savedNode"/>
</xsl:call-template>
</xsl:if>
<xsl:for-each select="exsl:node-set($precedingValue)/preceding">
<xsl:sort select="position()" data-type="number" order="descending"/>
<xsl:variable name="precedingTmp" select="normalize-space(.)"/>
<xsl:variable name="normalizedPreceding" select="translate($precedingTmp, ' ', '')"/>
<xsl:call-template name="model">
<xsl:with-param name="node" select="$savedNode"/>
<xsl:with-param name="preceding" select="$normalizedPreceding"/>
</xsl:call-template>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template name="model">
<xsl:param name="node"/>
<xsl:param name="preceding"/>
\<xsl:value-of select="$preceding"/><xsl:value-of select="$node/#name"/>
<xsl:for-each select="$node/items/item">
\<xsl:value-of select="$preceding"/><xsl:value-of select="$node/#name"/>\<xsl:value-of select="#name"/>
</xsl:for-each>
</xsl:template>
<xsl:template name="preceding">
<xsl:param name="modelName"/>
<xsl:for-each select="preceding::model">
<xsl:for-each select="submodels/submodel">
<xsl:choose>
<xsl:when test="contains(#ref, $modelName)">
<preceding>
<xsl:call-template name="preceding">
<xsl:with-param name="modelName" select="$modelName"></xsl:with-param>
</xsl:call-template>
<xsl:value-of select="../../#name"/>\
</preceding>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template match="items">
<xsl:for-each select="item">
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Input
<models>
<model name="AAA" root="true">
<items>
<item name="a"/>
<item name="b"/>
</items>
<submodels>
<submodel ref="BBB"/>
<submodel ref="CCC" />
</submodels>
</model>
<model name="BBB">
<items>
<item name="c"/>
<item name="d"/>
</items>
<submodels>
<submodel ref="CCC" />
</submodels>
</model>
<model name="CCC">
<items>
<item name="e"/>
</items>
</model>
</models>
Output
\AAA
\AAA\a
\AAA\b
\AAA\BBB
\AAA\BBB\c
\AAA\BBB\d
\AAA\BBB\CCC
\AAA\BBB\CCC\e
\AAA\CCC
\AAA\CCC\e
I hope it helps.

how to sum subdocument properties

There is list
<items>
<item parentid='1'>
<amount>3</amount>
</item>
<item parentid='2'>
<amount>1</amount>
</item>
</items>
and document:
<udata id='1'>
<price>10</price>
</udata>
<udata id='1'>
<price>20</price>
</udata>
How to sum price's all document's?
To sum count I use'd:
<xsl:value-of select="sum(items/item/amount)"/>
I'd use:
<xsl:apply-templates select="udata/items/item" mode='price2' />
<xsl:template mode='price2' match='item'>
<xsl:apply-templates select="document(concat('upage://', page/#parentId))" mode='price'>
<xsl:with-param select='amount' name='count'/>
</xsl:apply-templates>
</xsl:template>
<xsl:template mode='price' match='/'>
<xsl:param name='count'/>
<xsl:value-of select="$count * /udata/page/properties/group[#name='price_prop']/property[#name='price']/value"/>
</xsl:template>
In result i had:
3020
I need 50. How to do this?
Here is a sample assuming XSLT 2.0 (e.g. as possible with Saxon 9 or AltovaXML):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:param name="data-url" select="'test2012050103.xml'"/>
<xsl:variable name="data-doc" select="document($data-url)"/>
<xsl:key name="k1" match="udata" use="#id"/>
<xsl:template match="items">
<xsl:value-of select="sum(for $item in item return $item/amount * key('k1', $item/#parentid, $data-doc)/price)"/>
</xsl:template>
</xsl:stylesheet>
Example documents are
<items>
<item parentid='1'>
<amount>3</amount>
</item>
<item parentid='2'>
<amount>1</amount>
</item>
</items>
and
<root>
<udata id='1'>
<price>10</price>
</udata>
<udata id='2'>
<price>20</price>
</udata>
</root>
Output is 50.
[edit]Here is an XSLT 1.0 stylesheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:param name="data-url" select="'test2012050103.xml'"/>
<xsl:variable name="data-doc" select="document($data-url)"/>
<xsl:key name="k1" match="udata" use="#id"/>
<xsl:template match="items">
<xsl:call-template name="sum">
<xsl:with-param name="items" select="item"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="sum">
<xsl:param name="items" select="/.."/>
<xsl:param name="total" select="0"/>
<xsl:choose>
<xsl:when test="not($items)">
<xsl:value-of select="$total"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="price">
<xsl:for-each select="$data-doc">
<xsl:value-of select="$items[1]/amount * key('k1', $items[1]/#parentid)/price"/>
</xsl:for-each>
</xsl:variable>
<xsl:call-template name="sum">
<xsl:with-param name="items" select="$items[position() > 1]"/>
<xsl:with-param name="total" select="$total + $price"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Here is a shorter solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:variable name="vPrices" select=
"document('file:///c:/temp/delete/priceData.xml')/*/*"/>
<xsl:template match="/*">
<xsl:apply-templates select="item[1]"/>
</xsl:template>
<xsl:template match="item" name="sumProducts">
<xsl:param name="pAccum" select="0"/>
<xsl:variable name="vNewAccum" select=
"$pAccum +amount * $vPrices[#id = current()/#parentid]/price"/>
<xsl:if test="not(following-sibling::*)">
<xsl:value-of select="$vNewAccum"/>
</xsl:if>
<xsl:apply-templates select="following-sibling::*">
<xsl:with-param name="pAccum" select="$vNewAccum"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
When applied on the following XML document:
<items>
<item parentid='1'>
<amount>3</amount>
</item>
<item parentid='2'>
<amount>1</amount>
</item>
</items>
and having the file c:\temp\delete\priceData.xml contain:
<root>
<udata id='1'>
<price>10</price>
</udata>
<udata id='2'>
<price>20</price>
</udata>
</root>
then the wanted, correct result is produced:
50

XSLT 1.0 to iterate over several XML elements with comma delimited values

I have an XML document structured as follows
<items>
<item>
<name>item1</name>
<attributes>a,b,c,d</attributes>
</item>
<item>
<name>item2</name>
<attributes>c,d,e</attributes>
</item>
</items>
For each unique attribute value (delimited by commas) I need to list all item names associated with that value like so:
a : item1
b : item1
c : item1, item2
d : item1, item2
e : item2
My initial plan was to use a template to parse the attributes into Attribute nodes, surrounding each with appropriate tags, and then separating out the unique values with an XPATH expression like
Attribute[not(.=following::Attribute)]
but since the result of the template isn't a node-set that ever goes through an XML parser, I can't traverse it. I also tried exslt's node-set() function only to realize it does not allow me to traverse the individual Attribute nodes either.
At this point I'm at a loss for a simple way to do this and would really appreciate any help or ideas on how to proceed. Thanks!
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:ext="http://exslt.org/common">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kAtrByVal" match="attr" use="."/>
<xsl:template match="/">
<xsl:variable name="vrtfPass1">
<groups>
<xsl:apply-templates/>
</groups>
</xsl:variable>
<xsl:variable name="vPass1"
select="ext:node-set($vrtfPass1)"/>
<xsl:apply-templates select="$vPass1/*"/>
</xsl:template>
<xsl:template match="item">
<group name="{name}">
<xsl:apply-templates select="attributes"/>
</group>
</xsl:template>
<xsl:template match="attributes" name="tokenize">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length($pText)">
<xsl:variable name="vText" select=
"concat($pText,',')"/>
<attr>
<xsl:value-of select="substring-before($vText,',')"/>
</attr>
<xsl:call-template name="tokenize">
<xsl:with-param name="pText" select=
"substring-after($pText,',')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match=
"attr[generate-id()
=
generate-id(key('kAtrByVal',.)[1])
]
">
<xsl:value-of select="concat('
',.,': ')"/>
<xsl:for-each select="key('kAtrByVal',.)">
<xsl:value-of select="../#name"/>
<xsl:if test="not(position()=last())">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when applied on the provided XML document:
<items>
<item>
<name>item1</name>
<attributes>a,b,c,d</attributes>
</item>
<item>
<name>item2</name>
<attributes>c,d,e</attributes>
</item>
</items>
produces the wanted, correct result:
a: item1
b: item1
c: item1, item2
d: item1, item2
e: item2
Explanation:
Pass1: tokenization and end result:
<groups>
<group name="item1">
<attr>a</attr>
<attr>b</attr>
<attr>c</attr>
<attr>d</attr>
</group>
<group name="item2">
<attr>c</attr>
<attr>d</attr>
<attr>e</attr>
</group>
</groups>
.2. Pass2 takes the result of Pass1 (converted to a nodeset using the extension function ext:node-set()) as input, performs Muenchian grouping and produces the final, wanted result.
My first thought is to make two passes. First, tokenize the attributes elements using a (slightly) modified version of #Alejandro's answer to this previous question:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<items>
<xsl:apply-templates/>
</items>
</xsl:template>
<xsl:template match="item">
<item name="{name}">
<xsl:apply-templates select="attributes"/>
</item>
</xsl:template>
<xsl:template match="attributes" name="tokenize">
<xsl:param name="text" select="."/>
<xsl:param name="separator" select="','"/>
<xsl:choose>
<xsl:when test="not(contains($text, $separator))">
<val>
<xsl:value-of select="normalize-space($text)"/>
</val>
</xsl:when>
<xsl:otherwise>
<val>
<xsl:value-of select="normalize-space(
substring-before($text, $separator))"/>
</val>
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="substring-after(
$text, $separator)"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Which produces:
<items>
<item name="item1">
<val>a</val>
<val>b</val>
<val>c</val>
<val>d</val>
</item>
<item name="item2">
<val>c</val>
<val>d</val>
<val>e</val>
</item>
</items>
Then apply the following stylesheet to that output:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:key name="byVal" match="val" use="." />
<xsl:template match="val[generate-id() =
generate-id(key('byVal', .)[1])]">
<xsl:value-of select="." />
<xsl:text> : </xsl:text>
<xsl:apply-templates select="key('byVal', .)" mode="group" />
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="val" mode="group">
<xsl:value-of select="../#name" />
<xsl:if test="position() != last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="val" />
</xsl:stylesheet>
Producing:
a : item1
b : item1
c : item1, item2
d : item1, item2
e : item2
Doing this in one stylesheet would require more thought (or an extension function).