multi value grouping with xslt 2 - xslt-2.0

For starters my apology for the title, but I do not know a good way to describe my problem. A code sample will therefore make things more clear.
Assume I have following xml tree :
<root>
<node>
<value xml:lang="en">Some English Content</value>
<value xml:lang="fr">Some French Content</value>
<value xml:lang="de">Some German Content</value>
</node>
<node>
<value xml:lang="en">Some English Content</value>
<value xml:lang="de">Some German Content</value>
</node>
<node>
<value xml:lang="en">Some Other English Content</value>
<value xml:lang="fr">Some Other French Content</value>
<value xml:lang="de">Some Other German Content</value>
</node>
<node>
<value xml:lang="en">Some English Content</value>
<value xml:lang="fr">Some French Content</value>
<value xml:lang="de">Some German Content</value>
</node>
<node>
<value xml:lang="fr">Some French Content</value>
<value xml:lang="de">Some German Content</value>
</node>
</root>
So basically there are various nodesets, with a number of localized strings , and I want to group these sets based on the content. Node 1,2,4 and 5 are about the same topic, but not all strings might be available in all locales, so i can not really use a reference string (say English, as it's not available in node 5). Node 3 contains different content, so it should be part of a different group.
Sounds probably pretty complex, but this is the result I like to get (using xslt 2) :
<values>
<group>
<value xml:lang="en">Some English Content</value>
<value xml:lang="fr">Some French Content</value>
<value xml:lang="de">Some German Content</value>
</group>
<group>
<value xml:lang="en">Some Other English Content</value>
<value xml:lang="fr">Some Other French Content</value>
<value xml:lang="de">Some Other German Content</value>
</group>
</values>
Any idea on how to best deal with this ? Note that I can have up to 40 different languages in a node, and can have hundreds of nodes in a file, so resources can become an issue also.

I am not sure I understand your requirement fully but I wrote some code that first tries to fill up node elements with values for the missing languages and then to group on the filled elements:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf">
<xsl:param name="sep" as="xs:string" select="'|'"/>
<xsl:output indent="yes"/>
<xsl:variable name="main-doc" select="/"/>
<xsl:variable name="languages" as="xs:string*">
<xsl:perform-sort select="distinct-values(root/node/value/#xml:lang)">
<xsl:sort select="."/>
</xsl:perform-sort>
</xsl:variable>
<xsl:key name="k1" match="node/value" use="concat(#xml:lang, $sep, .)"/>
<xsl:template match="root">
<values>
<xsl:variable name="filled" as="element(node)*">
<xsl:apply-templates select="node" mode="fill"/>
</xsl:variable>
<xsl:for-each-group select="$filled" group-by="string-join(value, $sep)">
<group>
<xsl:copy-of select="value"/>
</group>
</xsl:for-each-group>
</values>
</xsl:template>
<xsl:template match="node" mode="fill">
<xsl:copy>
<xsl:variable name="this" as="element(node)" select="."/>
<xsl:for-each select="$languages">
<value xml:lang="{.}">
<xsl:value-of
select="if ($this/value[lang(current())])
then $this/value[lang(current())]
else (key('k1',
concat($this/value[1]/#xml:lang, $sep, $this/value[1]),
$main-doc)/../value[lang(current())])[1]"/>
</value>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
As you can see, I needed some order to fill up the elements so I sorted on the distinct #xml:lang, I am not sure you want that. With that approach the output for the input you have posted with Saxon 9.5 is
<values>
<group>
<value xml:lang="de">Some German Content</value>
<value xml:lang="en">Some English Content</value>
<value xml:lang="fr">Some French Content</value>
</group>
<group>
<value xml:lang="de">Some Other German Content</value>
<value xml:lang="en">Some Other English Content</value>
<value xml:lang="fr">Some Other French Content</value>
</group>
</values>
I am also not sure which strategy is the intended one to fill elements (see also the comment I posted). In the end I decided to key the node/value elements on the concatenation of #xml:lang and their content, then when filling up elements where a language is missing I simply match on the first value child a node has. So that basically means if some node has a first value xml:lang="foo" with contents bar, the match is simply on that language foo and contents bar and we copy over the value contents for the missing language.
If you don't want the sorting and you can live with the order of the initial sequence of attributes then you can omit the sorting, the stylesheet then is
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf">
<xsl:param name="sep" as="xs:string" select="'|'"/>
<xsl:output indent="yes"/>
<xsl:variable name="main-doc" select="/"/>
<xsl:variable name="languages" as="xs:string*" select="distinct-values(root/node/value/#xml:lang)"/>
<xsl:key name="k1" match="node/value" use="concat(#xml:lang, $sep, .)"/>
<xsl:template match="root">
<values>
<xsl:variable name="filled" as="element(node)*">
<xsl:apply-templates select="node" mode="fill"/>
</xsl:variable>
<xsl:for-each-group select="$filled" group-by="string-join(value, $sep)">
<group>
<xsl:copy-of select="value"/>
</group>
</xsl:for-each-group>
</values>
</xsl:template>
<xsl:template match="node" mode="fill">
<xsl:copy>
<xsl:variable name="this" as="element(node)" select="."/>
<xsl:for-each select="$languages">
<value xml:lang="{.}">
<xsl:value-of
select="if ($this/value[lang(current())])
then $this/value[lang(current())]
else (key('k1',
concat($this/value[1]/#xml:lang, $sep, $this/value[1]),
$main-doc)/../value[lang(current())])[1]"/>
</value>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
That way the output is as you asked for:
<values>
<group>
<value xml:lang="en">Some English Content</value>
<value xml:lang="fr">Some French Content</value>
<value xml:lang="de">Some German Content</value>
</group>
<group>
<value xml:lang="en">Some Other English Content</value>
<value xml:lang="fr">Some Other French Content</value>
<value xml:lang="de">Some Other German Content</value>
</group>
</values>

Related

How match non string in xslt

I want to match non string in xslt 2.0.
<td><list>
<list-item>
<p>aaaa</p>
<list>
<list-item>
<p>bbb</p>
</list-item>
</list>
</list-item>
</list>
</td>
I have done a template like this
<xsl:template match="td">
<para>
<xsl:apply-templates/>
</tps:p>
</xsl:template>
When input xml like below:
<td><list>
<list-item>
<p>aaaa</p>
<list>
<list-item>
<p>bbb</p>
</list-item>
</list>
</list-item>
</list>
kmkmkmk
</td>
Output(correct output):
<para>kmkmkmk</para>
When input xml like below (new line between td and list):
<td><list>
<list-item>
<p>aaaa</p>
<list>
<list-item>
<p>bbb</p>
</list-item>
</list>
</list-item>
</list>
</td>
Output(Incorrect output):
<para> </para>
But When input xml like below( no new line between td and list ):
<td><list>
<list-item>
<p>aaaa</p>
<list>
<list-item>
<p>bbb</p>
</list-item>
</list>
</list-item>
</list></td>
Output(correct output):
nothing display
I want to do when td has no any text nothing display and display only display td has text.
Why when there is a new line between <td> and <list> not working well and where is no new line working wells. How can I fix this?
Your samples are not minimal and complete but to transform a td element node which has non-whitespace text child contents you can use
<xsl:template match="td[text()[normalize-space()]]">
<para>
<xsl:apply-templates/>
</para>
</xsl:template>
Whether you additionally need <xsl:template match="td[not(text()[normalize-space()])]"/> or <xsl:template match="td[not(text()[normalize-space()])]"><xsl:apply-templates/></xsl:template> depends on how you set up the rest of the transformation.

Having difficulty in looping partNumber XSLT being applied shown below

Sample SOAP Input XML Given below:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Body>
<PullCustomerPartsPricingResponse xmlns="http://cdx.dealerbuilt.com/Api/0.99/">
<PullCustomerPartsPricingResult xmlns:a="http://schemas.datacontract.org/2004/07/DealerBuilt.BaseApi" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:CustomerPart>
<a:Placement>
<a:GroupId>10</a:GroupId>
</a:Placement>
<a:Attributes xmlns:b="http://schemas.datacontract.org/2004/07/DealerBuilt.Models.Parts">
<b:Description>PAD SET, RR.</b:Description>
<b:PartNumber>31500SB2100M</b:PartNumber>
<b:PartNumberFormatted>31500-SB2-100M</b:PartNumberFormatted>
</a:Attributes>
</a:CustomerPart>
<a:CustomerPart>
<a:Placement>
<a:GroupId>10</a:GroupId>
</a:Placement>
<a:Attributes xmlns:b="http://schemas.datacontract.org/2004/07/DealerBuilt.Models.Parts">
<b:Description>Kite SET, RR.</b:Description>
<b:PartNumber>60211T7J305ZZ</b:PartNumber>
</a:Attributes>
</a:CustomerPart>
</PullCustomerPartsPricingResult>
</PullCustomerPartsPricingResponse>
</s:Body>
</s:Envelope>
XSLT Code being applied Shown below:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<root xmlns="http://www.dataprint.com/global/3.0/rest/">
<xsl:for-each select="/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='PullCustomerPartsPricingResponse']/*[local-name()='PullCustomerPartsPricingResult']/*[local-name()='CustomerPart']">
<partDetail>
<partNumber>
<xsl:value-of select="/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='PullCustomerPartsPricingResponse']/*[local-name()='PullCustomerPartsPricingResult']/*[local-name()='CustomerPart']/*[local-name()='Attributes'] /*[local-name()='PartNumber']" />
</partNumber>
<partDescription>
<xsl:value-of select="/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='PullCustomerPartsPricingResponse']/*[local-name()='PullCustomerPartsPricingResult']/*[local-name()='CustomerPart']/*[local-name()='Attributes'] /*[local-name()='Description']" />
</partDescription>
</partDetail>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
My Current sample Output Shown below:
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="http://www.dataprint.com/global/3.0/rest/">
<results>
<partDetail>
<partNumber>31500SB2100M</partNumber>
<partDescription>PAD SET, RR.</partDescription>
</partDetail>
</results>
<results>
<partDetail>
<partNumber>31500SB2100M</partNumber>
<partDescription>PAD SET, RR.</partDescription>
</partDetail>
</results>
</root>
My Desired Output shown below:
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="http://www.dataprint.com/global/3.0/rest/">
<results>
<partDetail>
<partNumber>31500SB2100M</partNumber>
<partDescription>PAD SET, RR.</partDescription>
</partDetail>
</results>
<results>
<partDetail>
<partNumber>60211T7J305ZZ</partNumber>
<partDescription>Kite SET, RR.</partDescription>
</partDetail>
</results>
</root>
I'm trying to loop element " a:CustomerPart" to print PartNumber and b:Descriptionas Output but first a:CustomerPart data is getting repeated Twice.I Undestand there is code change is required,please help on this.
Eiríkr Útlendi your patience/suggestions is much appreciated
Inside the xsl:for-each, the selected CustomerPart item becomes the context item. You should then select children/descendants of this CustomerPart using a relative path starting at this context item, not an absolute path starting at the root of the document (/).
That is, you should replace
<xsl:value-of select="/*[local-name()='Envelope']/*[local-name()='Body']
/*[local-name()='PullCustomerPartsPricingResponse']
/*[local-name()='PullCustomerPartsPricingResult']
/*[local-name()='CustomerPart']
/*[local-name()='Attributes']
/*[local-name()='PartNumber']" />
by
<xsl:value-of select="*[local-name()='Attributes'] /*[local-name()='PartNumber']" />
or better, by
<xsl:value-of select="*:Attributes/*:PartNumber" />

XSLT 2.0 performance of convert flat XML data to heirarchical?

I have data as follows:
<Root>
<Node>
<Region>Central</Region>
<Provider>A</Provider>
<Value>100</Value>
</Node>
<Node>
<Region>Central</Region>
<Provider>B</Provider>
<Value>200</Value>
</Node
<Node>
<Region>Central</Region>
<Provider>B</Provider>
<Value>250</Value>
</Node>
<Node>
<Region>Eastern</Region>
<Provider>C</Provider>
<Value>50</Value>
</Node>
</Root>
And trying to transform as:
<Root>
<Region>
<Name>Central</Name>
<Provider>
<Name>A</Name>
<Item>
<Value>100</Value>
</Item>
</Provider>
<Provider>
<Name>B</Name>
<Item>
<Value>200</Value>
</Item>
<Item>
<Value>250</Value>
</Item>
</Provider>
</Region>
<Region>
<Name>Eastern</Name>
<Provider>
<Name>C</Name>
<Item>
<Value>50</Value>
</Item>
</Provider>
</Region>
</Root>
In the past I have used Muenchian Method, but I am trying to use XSLT 2.0 construct xsl:for-each-group to accomplish the same thing without success so far.
Is it possible to use xsl:for-each-group to accomplish the above? How?
Edit
This is what I have so far, which appears to work. But, I am not sure about efficiency - specifically does using such constructs force the parser to read the entire document and sort it? The data coming in is already sorted the way I want.
<xsl:template match="Root">
<xsl:element name="Root">
<xsl:for-each-group select="Node" group-by="Region">
<xsl:element name="Region">
<xsl:element name="Name">
<xsl:value-of select="Region" />
</xsl:element>
<xsl:for-each-group select="current-group()" group-by="Provider">
<xsl:element name="Provider">
<xsl:for-each select="current-group()">
<xsl:element name="Item">
<xsl:value-of select="Value" />
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each-group>
</xsl:element>
</xsl:for-each-group>
</xsl:element>
</xsl:template>

xmlproperty and retrieving property value of xml element if specific attribute is present/set

My Xml looks like:
<root>
<foo location="bar"/>
<foo location="in" flag="123"/>
<foo location="pak"/>
<foo location="us" flag="256"/>
<foo location="blah"/>
</root>
For foo xml element flag is optional attribute.
And when I say:
<xmlproperty file="${base.dir}/build/my.xml" keeproot="false"/>
<echo message="foo(location) : ${foo(location)}"/>
prints all locations :
foo(location) : bar,in,pak,us,blah
Is there a way to get locations only if flag is set to some value?
Is there a way to get locations only if flag is set to some value?
Not with xmlproperty, no, as that will always conflate values that have the same tag name. But xmltask can do what you need as it supports the full power of XPath:
<taskdef name="xmltask" classname="com.oopsconsultancy.xmltask.ant.XmlTask">
<classpath path="xmltask.jar" />
</taskdef>
<xmltask source="${base.dir}/build/my.xml">
<copy path="/root/foo[#flag='123' or #flag='256']/#location"
property="foo.location"
append="true" propertySeparator="," />
</xmltask>
<echo>${foo.location}</echo><!-- prints in,us -->
If you absolutely cannot use third-party tasks then I'd probably approach the problem by using a simple XSLT to extract just the bits of the XML that you do want into another file:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="targetFlag" />
<xsl:template name="ident" match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()" /></xsl:copy>
</xsl:template>
<xsl:template match="foo">
<xsl:if test="#flag = $targetFlag">
<xsl:call-template name="ident" />
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Call this with the xslt task
<xslt in="${base.dir}/build/my.xml" out="filtered.xml" style="extract.xsl">
<param name="targetFlag" expression="123" />
</xslt>
This will create filtered.xml containing just
<root>
<foo location="in" flag="123"/>
</root>
(modulo changes in whitespace) and you can load this using xmlproperty in the normal way.

xslt Ant Task not passing parameters to my stylesheet

I have a style sheet like this
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="testParam"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="resources/integer[#name='LOG_LEVEL']/text()">
<xsl:value-of select="$testParam"/>
</xsl:template>
</xsl:stylesheet>
And I have an input xml like this:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="LOG_LEVEL">3</integer>
<string name="app_name">Test Application</string>
</resources>
But when I try to call an xslt transform in ant using this:
<xslt in="in.xml" out="out.xml" style="style_above.xsl">
<outputproperty name="method" value="xml"/>
<outputproperty name="encoding" value="UTF-8"/>
<outputproperty name="indent" value="yes"/>
<param name="testParam" expression="test"/>
</xslt>
I get the following:
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<integer name="LOG_LEVEL"/>
<string name="app_name">Test Application</string>
</resources>
it doesn't seem to be changing my xslt parameter to the value I specify in my ant target
Yep, i figured out the problem was a different thing. Was about to update this was too late. I defined the xslt task in a macro and have a optional element also named param and that was the culprit. Thanks

Resources