Using XSL:sort within xsl:if - xslt

I'm just figuring out how XSL works... mostly. I have 3 nodes (STUX, KbnApp, KbnStorge) that I want to transform in the same way except for how their list of "Servers" children are sorted. I wanted to do this with a single template that uses an xsl:if to choose an alternate sorting method for the "STUX". But I couldn't get it to work. The STUX Servers should end up in descending order, the others should stay in whatever order they are currently in.
Here is my XML
<?xml version="1.0" encoding="UTF-8"?>
<Categories>
<STUX>
<Servers>stuxsh01</Servers>
<Servers>stuxsh03</Servers>
<Servers>stuxsh02</Servers>
<UnitTest>Pass</UnitTest>
</STUX>
<KbnApp>
<Servers>stks01</Servers>
<Servers>stks03</Servers>
<Servers>stks02</Servers>
<UnitTest>Pass</UnitTest>
</KbnApp>
<KbnStorage>
<Servers>stksnfs01</Servers>
<Servers>stksnfs02</Servers>
<UnitTest>Fail</UnitTest>
</KbnStorage>
</Categories>
This transform gives me the result that I want, but I've had to make two templates that are almost identical, wasting lines. I think there should be a way to do this with one template and an xsl:if with an xsl:sort inside it. Can anyone tell me how I would format XSL to do that? Every time I've tried putting an xsl:if the XSL can't be parsed/won't apply and I don't know why. PS I'm SURE there is things I've done here that aren't proper, but by golly I got it to work :) I'm definitely open to learning how to do it better!
EDIT: Corrected a typo
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<categories>
<countCategories><xsl:value-of select="count(child::*)" /></countCategories>
<xsl:apply-templates />
</categories>
</xsl:template>
<xsl:template match="KbnApp|KbnStorage">
<Category>
<Name><xsl:value-of select="name()" /></Name>
<countServers><xsl:value-of select="count(Servers)" /></countServers>
<xsl:copy-of select="UnitTest" />
<xsl:for-each select="Servers">
<Server><xsl:value-of select="."/></Server>
</xsl:for-each>
</Category>
</xsl:template>
<xsl:template match="STUX">
<Category>
<Name><xsl:value-of select="name()" /></Name>
<countServers><xsl:value-of select="count(Servers)" /></countServers>
<xsl:copy-of select="UnitTest" />
<xsl:for-each select="Servers">
<xsl:sort order="descending"/>
<Server><xsl:value-of select="."/></Server>
</xsl:for-each>
</Category>
</xsl:template>
</xsl:stylesheet>
For reference...The desired result
<categories>
<countCategories>1</countCategories>
<Category>
<Name>STUX</Name>
<countServers>3</countServers>
<UnitTest>Pass</UnitTest>
<Server>stuxsh03</Server>
<Server>stuxsh02</Server>
<Server>stuxsh01</Server>
</Category>
<Category>
<Name>KbnApp</Name>
<countServers>3</countServers>
<UnitTest>Pass</UnitTest>
<Server>stks01</Server>
<Server>stks03</Server>
<Server>stks02</Server>
</Category>
<Category>
<Name>KbnStorage</Name>
<countServers>2</countServers>
<UnitTest>Fail</UnitTest>
<Server>stksnfs01</Server>
<Server>stksnfs02</Server>
</Category>
</categories>

Try changing:
<xsl:sort order="descending"/>
to:
<xsl:sort select="self::*[parent::STUX]" order="descending"/>
Then you can use this in a template that matches all your categories, and have only the STUX category sorted.

Related

XSLT 1.0 template Muenchian grouping

I use a tool where a xslt template is pre-defined and it is not desirable to remove it.
<xsl:template match="/">
<Msg xmlns="urn:com.sap.b1i.vplatform:entity">
<xsl:copy-of select="/vpf:Msg/#*"></xsl:copy-of>
<xsl:copy-of select="/vpf:Msg/vpf:Header"></xsl:copy-of>
<Body>
<xsl:copy-of select="/vpf:Msg/vpf:Body/*"></xsl:copy-of>
<Payload Role="X" id="{$atom}">
<xsl:call-template name="transform"></xsl:call-template>
</Payload>
</Body>
</Msg>
<xsl:template name="transform">
<!-- In this area we write our xpath and build the xml-file-->
</xsl:template>
Now I want to use the Muenchian grouping method. But for this method you also need to define a template en key. Like this:
<xsl:key name="KeyOrder" match="/vpf:Msg/vpf:Body/vpf:Payload[#id='atom8']/Orders/jdbc:Row" use="jdbc:RecId2" />
<xsl:template match="Orders" >
<Documents>
<xsl:for-each select="jdbc:Row[count(. | key('KeyOrder', jdbc:RecId2)[1]) = 1]">
<xsl:sort select="jdbc:RecId2" />
<Document>
<xsl:copy-of select="jdbc:RecId2" />
<xsl:for-each select="key('KeyOrder', jdbc:RecId2)">
<xsl:sort select="jdbc:OrderNrRef" />
<xsl:copy-of select="." />
</xsl:for-each>
</Document>
</xsl:for-each>
</Documents>
</xsl:template>
The problem is that the 2 templates won't work togetheter the way I copied it here. That means, I don't get the Muenchian grouping results. It's only works when I 'disable' xsl:template match="/", but then I lose a lot of other information which is necessary further in the process.
So how can I get in my XML file the results of both templates?

Sort data in the xml alphabetical order

Input XML :
<?xml version="1.0" encoding="utf-8" ?>
<infoset>
<info>
<title>Bill</title>
<group>
<code>state</code>
</group>
</info>
<info>
<title>Auto</title>
<group>
<code>state</code>
</group>
</info>
<info>
<title>Auto2</title>
</info>
<info>
<title>Auto3</title>
</info>
<info>
<title>Auto5</title>
</info>
<info>
<title>Certificate</title>
<group>
<code>Auto4</code>
</group>
</info>
</infoset>
Expected output :
A
Auto2
Auto3
Auto4
Certificate
Auto5
S
state
Auto
Bill
I need to arrange the title and code in alphabetical order.If the info has group the tile should come under the group. I am using visual studio2010 , xslt1.0 Processor and xml editor.
Sorting in XSLT is straight-forward. What you are actually really needing to know is how to 'group' items. As Michael Kay commented, this is much easier in XSLT 2.0 than in XSLT 1.0. In XSLT 1.0 you tend to use the Muenchian Grouping method, which appears confusing when you first see it, but is generally the most efficient way of doing it.
From the looks of your output, you are doing two lots of grouping. Firstly, by the first letter, then by either group/code (if it exists), or title.
Muenchian Grouping works by defining a key, to enable quick look up of all items in a 'group'. For the first letter of eithe group/code or title, you would define it like so
<xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
(Note: This is case sensitive, so you may need to use the 'translate' function if you can have a mix of lower and upper case start letters).
If group/code exists, it will use the first letter of that, otherwise it will pick up the first letter of the title.
For the group/code or title itself, the key would be as follows
<xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
So, it only uses "title" elements where there is no "group" element present.
To get the distinct first letters for your first grouping, you select all the info elements and check whether they are the first element in the key for their given letter. This is done like so
<xsl:apply-templates
select="info
[generate-id()
= generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]"
mode="letter">
<xsl:sort select="substring(concat(group/code, title), 1, 1)" />
</xsl:apply-templates>
The 'mode' is used here because the final XSLT will have to templates matching info.
Within the matching template, to group by either code/group or title you can then do this
<xsl:apply-templates
select="key('letter', $letter)
[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
<xsl:sort select="title[not(../group)]|group/code" />
</xsl:apply-templates>
And finally, to output all the elements within the final group, you would just use the key again
<xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
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="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
<xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
<xsl:template match="/*">
<xsl:apply-templates select="info[generate-id() = generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]" mode="letter">
<xsl:sort select="substring(concat(group/code, title), 1, 1)" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="info" mode="letter">
<xsl:variable name="letter" select="substring(concat(group/code, title), 1, 1)" />
<xsl:value-of select="concat($letter, '
')" />
<xsl:apply-templates select="key('letter', $letter)[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
<xsl:sort select="title[not(../group)]|group/code" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="info">
<xsl:variable name="value" select="title[not(../group)]|group/code" />
<xsl:value-of select="concat($value, '
')" />
<xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
<xsl:sort select="." />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="title">
<xsl:value-of select="concat(' ', ., '
')" />
</xsl:template>
</xsl:stylesheet>
When applied to your XML, the following is output
A
Auto2
Auto3
Auto4
Certificate
Auto5
s
state
Auto
Bill

how to call my template and my function sequentially in xslt 2.0?

I am using xslt2.0 for convert one xml format to another xml format. This is my sample xml document.
<w:document>
<w:body>
<w:p>Para1</w:p>
<w:p>Para2</w:p>
<w:p>Para3</w:p>
<w:p>Para4</w:p>
</w:body>
</w:document>
Initially this is my xml format.so, i handled each and every <w:p> elements through my function in xslt given below...
<xsl:template match="document">
<Document>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
So,In that xslt function, i have coded how to reformat those elements.It's working fine...
But now,Xml format is restructured like given below...
<w:document>
<w:body>
<w:tbl><!--some text with children elements--></w:tbl>
<w:tbl><!--some text with children elements--></w:tbl>
<w:p>Para1</w:p>
<w:p>Para2</w:p>
<w:p>Para3</w:p>
<w:p>Para4</w:p>
</w:body>
</w:document>
So, As of now i have to handle both and elements in a same sequence.....
What i want to do is,
If i encounter elemtents then i have to call my template given below...
<xsl:template match="document">
<Document>
<xsl:for-each select="w:tbl">
<xsl:apply-templates select="w:tbl">
</xsl:apply-templates>
</xsl:for-each>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
<xsl:template match="w:tbl">
<!--xslt code here -->
</xsl:template>
But the for-each statement is not executed when I trying transformation...
So, Please guide me to get out of this issue...
I think instead of
<xsl:template match="document">
<Document>
<xsl:for-each select="w:tbl">
<xsl:apply-templates select="w:tbl">
</xsl:apply-templates>
</xsl:for-each>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
you simply want
<xsl:template match="document">
<Document>
<xsl:apply-templates select="w:body/w:tbl"/>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
If that does not do what you want then please show the result you want.

XSLT - how to apply a template to every node of the type?

I am quite new to xsl and functional programming, so I'll be grateful for help on this one:
I have a template that transforms some xml and provides an output. The problem is that there are many elements of type xs:date, all in different contexts, that must be localized. I use a concatenation of substrings of these xs:dates to produce a localized date pattern strings.
As you can guess this causes a lot of copy-paste "substring-this and substring-that". How can I write a template that will automatically transform all the elements of type xs:date to localized strings preserving all the context-aware transformations?
My xsl is something like this:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" encoding="utf-8"/>
<xsl:template match="/">
...
<input value="{substring(/select/a/date 9,2)}.{substring(/select/a/date, 6,2)}.{substring(/select/a/date 1,4)}">
...
<!-- assume that following examples are also with substrings -->
<div><xsl:value-of select="different-path/to_date"/></div>
...
<table>
<tr><td><xsl:value-of select="path/to/another/date"/></td></tr>
</table>
<apply-templates/>
</xsl:template>
<xsl:template match="something else">
<!-- more dates here -->
</xsl:template>
</xsl:stylesheet>
I hope I managed to make my question clear =)
UPD: Here is an example of xml:
<REQUEST>
<header>
<... />
<ref>
<ref_date type="xs:date">1970-01-01</ref_date>
</ref>
</header>
<general>
<.../>
<info>
<.../>
<date type="xs:date">1970-01-01</date>
<ExpireDate type="xs:date">1970-01-01</ExpireDate>
<RealDate type="xs:date">1970-01-01</RealDate>
<templateDetails>template details</templateDetails>
<effectiveDate type="xs:date">1970-01-01</effectiveDate>
</info>
<party>
<.../>
<date type="xs:date">1970-01-01</date>
</party>
<!-- many other parts of such kind -->
</general>
</REQUEST>
As for the output, there are many different options. The main thing is that these values must be set as a value of different html objects, such as tables, input fields and so on. You can see an example in the xsl listing.
P.S. I'm using xsl 1.0.
If you did a schema-aware XSLT 2.0 transformation, you wouldn't need all those type='xs:date' attributes: defining it in the schema as a date would be enough. You could then match the attributes with
<xsl:template match="attribute(*, xs:date)">
What you could do is add a template to match any element which has an #type attribute of 'xs:date', and do you substring manipulation in there
<xsl:template match="*[#type='xs:date']">
<xsl:value-of select="translate(., '-', '/')" />
</xsl:template>
In this case I am just replacing the hyphens by slashes as an example.
Then, instead of using xsl:value-of....
<div><xsl:value-of select="different-path/to_date"/></div>
You could use xsl:apply-templates
<div><xsl:apply-templates select="different-path/to_date"/></div>
Consider this XSLT as an example
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="*[#type='xs:date']">
<xsl:copy>
<xsl:value-of select="translate(., '-', '/')" />
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
In this case, all this XSLT is doing is copying the XML document as-is, but changing the date elements.
If you wanted to use the date template for other elements, or values, you could also make it a named-template, like so
<xsl:template match="*[#type='xs:date']" name="date">
<xsl:param name="date" select="." />
<xsl:value-of select="translate($date, '-', '/')" />
</xsl:template>
This would allow you to also call it much like a function. For example, to format a data and add as an attribute you could do the following:
<input>
<xsl:attribute name="value">
<xsl:call-template name="date">
<xsl:with-param name="date" select="/select/a/date" />
</xsl:call-template>
</xsl:attribute>
</input>

Can input parameters be transformed into xml structure?

I need to transform input paramater string like "1-410000 54-420987 63-32000" into the structure (like below) inside xslt to use its data later in xslt:
<config:categories>
<category>
<value>410000</value>
<label>1</label>
</category>
<category>
<value>420987</value>
<label>54</label>
</category>
<category>
<value>32000</value>
<label>63</label>
</category>
</config:categories>
P.S.
Are there any other options to parse string like "1-410000 54-420987 63-32000" in order to use its data in xslt to extract the right part (after the '-') if the left part is found in input document?
This transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:config="some:config" exclude-result-prefixes="config">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="pData" select="'1-410000 54-420987 63-32000'"/>
<xsl:template match="/*">
<config:categories>
<xsl:call-template name="gen"/>
</config:categories>
</xsl:template>
<xsl:template name="gen">
<xsl:param name="pGen" select="$pData"/>
<xsl:if test="$pGen">
<xsl:variable name="vChunk" select=
"substring-before(concat($pGen, ' '), ' ')"/>
<category>
<value><xsl:value-of select="substring-after($vChunk,'-')"/></value>
<label><xsl:value-of select="substring-before($vChunk,'-')"/></label>
</category>
<xsl:call-template name="gen">
<xsl:with-param name="pGen" select="substring-after($pGen, ' ')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on any XML document (not used), produces the wanted, correct result:
<config:categories xmlns:config="some:config">
<category>
<value>410000</value>
<label>1</label>
</category>
<category>
<value>420987</value>
<label>54</label>
</category>
<category>
<value>32000</value>
<label>63</label>
</category>
</config:categories>
Explanation:
Proper use of substring-before() and substring-after() plus recursion.
As Dimitre shows, parsing strings in XSLT 1.0 is quite cumbersome. This is an area where XSLT 2.0 is far superior. It can be done like this:
<categories>
<xsl:for-each select="tokenize($param, '\s+')">
<category>
<label><xsl:value-of select="substring-after(., '-')"/></label>
<value><xsl:value-of select="substring-before(., '-')"/></value>
</category>
</xsl:for-each>
</categories>
And of course in XSLT 2.0 you can then use the categories structure as a first-class node value without having to use the node-set() extension to get inside it.
to make XML from a string I think you have to use Java. I am not aware of an XSLT functioniality to achieve that.
For your second question:
There is substring(), string-length(), substring-before() and substring-after() that might help with your request.
Please refer: http://www.w3schools.com/xpath/xpath_functions.asp