build node-set variable from result tree fragment using <xsl:choose> - xslt

Is it possible to create a node-set variable from an rtf using xsl:choose (for use in MSXML engine)?
I have the following construct:
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:variable name="colorList" select="msxsl:node-set($std:colorList)"/>
<xsl:for-each select="$colorList/color">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="colorList" select="$std:colorList"/>
<xsl:for-each select="$colorList/color">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
std:colorList being the tree fragment of course.
The above works fine, and is OK because the code is the same for the two alternatives but is not that large.
But for larger code fragments I wonder whether it is possible to avoid duplicating code by first declaring the variable based on the rtf, and then perform the code; something like
<xsl:variable name="colorList">
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:copy-of select="msxsl:node-set($std:colorList)"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$std:colorList"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:for-each select="$colorList/color">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:for-each>
But this does not work properly: MSXML complains about colorList not being a node-set, so it cannot be used in the xsl:for-each.
XSL transformation failed due to following error:
Expression must evaluate to a node-set.
-->$colorList<--/color
Note that in the working example, this error did not occur because of "copying" std:colorList into the colorList variable. Apparently it is an xsl parsing error, not a runtime one.
Should I use something else than xsl:copy-of? Or is there another way to achieve the same?
In case you wonder, std:colorList contents are as follows:
<std:colorList>
<color testid="111">#FF0000</color>
<color testid="999">#FFFF00</color>
</std:colorList>

Unfortunately in XSLT 1.0, when xsl:variable has contained instructions rather than a select attribute, the result is always an RTF. So your careful attempts to convert the RTF to a node-set come to nothing, because it's converted straight back again.
I'm afraid there's no clean workaround (other than moving to XSLT 2.0, of course). I would suggest structuring the code like this:
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:apply-templates select="msxsl:node-set($std:colorList)/color" mode="z"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="$std:colorList/color" mode="z"/>
</xsl:otherwise>
</xsl:choose>
<xsl:template match="color" mode="z">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:template>

Just for the record, I add the final solution below. It is slightly different from what Michael proposed, adding a copy of the RTF to a variable before applying the template.
This is because otherwise MSXML still errors during xsl parsing (apparently it checks the apply-templates select value and concludes it is not correct when it is an RTF instead of a node-set. And, as Michael said, xsl:variable select attribute does just that: converting an RTF to a node-set.
<xsl:choose>
<xsl:when test="function-available('msxsl:node-set')">
<xsl:apply-templates select="msxsl:node-set($std:colorList)/color" mode="addTRclassToCSS"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="colorList" select="$std:colorList"/>
<xsl:apply-templates select="$colorList" mode="addTRclassToCSS"/>
</xsl:otherwise>
</xsl:choose>
<xsl:template match="color" mode="addTRclassToCSS">
tr.testid<xsl:value-of select="#testid"/> {
color:<xsl:value-of select="."/>;
}
</xsl:template>

Related

Duplicates in a map

I currently have an XSLT function that loads key=value pairs from a text file into a map.
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
The function works fine unless the user tries to load a file containing duplicates, in which case the XSLT transform fails with an error (expected behaviour):
Error evaluating (map:merge(...)) on line xyz column xy of xyz.xsl:
XTDE3365: Duplicate key in constructed map: {keyInError}
Is there a way I could catch this case and keep the transformation from aborting, something like this :
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:choose>
<xsl:when test="...map contains key...">
<xsl:message>Map already contains key. Please check input file.</xsl:message>
</xsl:when>
<xsl:otherwise>
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
I see that there is something implemented for a future XSLT 4.0 release (Saxon - Controlling duplicates on xsl:map) but I would like to stick to XSLT 3.0 for the time being.
Thanks.
To add to Martin Honnen's suggestions, you could use xsl:iterate instead of xsl:for-each, passing the map as a parameter, which would allow you to inspect the map before adding another entry to it.
<xsl:iterate select="...">
<xsl:param name="map" select="map{}"/>
<xsl:choose>
<xsl:when test="map:contains($map, ...)">...</xsl:when>
<xsl:otherwise>
<xsl:next-iteration>
<xsl:with-param name="map" select="map:put($map, ..., ...)"/>
Well, both map:merge in XPath 3.1 or of course grouping with e.g.
<xsl:for-each-group select="unparsed-text-lines($inputFile,$fileEncoding)[contains(.,'=') and not(starts-with(.,'#'))]" group-by="substring-before(., '=')">
<xsl:map-entry key="current-grouping-key()" select="substring-after(., '=')"/>
<xsl:if test="current-group()[2]">
<xsl:message>..</xsl:message>
</xsl:if>
</xsl:for-each-group>
allow you more control than your approach without having to wait for XSLT 4 or trying to use experimental extensions.

Populate a variable with a subtree

in a version="2.0" stylesheet:
the following code produces the correct output
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer/t:Repository" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj"/>
but this one does not
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
How can I get the second code to run as expected ?
If needed, is there a solution in v3 ?
this code does not run either
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
relevant xml input
<Flow>
<FHeader>
<Producer>
<Repository>tests.com</Repository>
</Producer>
</FHeader>
</Flow>
You can simply select <xsl:variable name="obj" select="/t:Flow/t:FHeader/t:Producer/t:Repository[current()/t:ReferencedObjectType='Asset']"/>. Or, as Tim already commented, use xsl:copy-of, also taking into account that you then later on need e.g. $obj/t:Producer/t:Repository to select the right level.
Or learn about the as attribute and use e.g. <xsl:variable name="obj" as="element()*">...<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer"/> ...</xsl:variable>, then you later on can use e.g. $obj/t:Repository.
There is also xsl:sequence to select input nodes instead of copying them, in particular with xsl:variable if you use the as attribute. This might consume less memory.
Furthermore XPath 2 and later have if (condition-expression) then expression else expression conditional expressions at the expression level so you might not need XSLT with xsl:choose/xsl:when but could use the <xsl:variable name="obj" select="if (t:ReferencedObjectType='Asset']) then /t:Flow/t:FHeader/t:Producer else if (...) then ... else ()"/>, that way you would select e.g. an input t:Producer element anyway and if you use the variable you can directly select the t:Repository child.

Working on Variable in XSL loop to hold dynamic value

Working on the xsl and i am looking for variable inside a loop to reset on new record.
Fetching records from oracle table
<xsl:variable name="curr_temp_emp_no" select="'##'"/>
<xsl:for-each select="/data/test/loof/super_incompleted">
<xsl:variable name="curr_temp_emp_no2" select="emp_no"/>
<xsl:choose>
<xsl:when test="$curr_temp_emp_no2 != $curr_temp_emp_no">
<xsl:value-of select="emp_no"/><fo:inline> - </fo:inline><xsl:value-of select="variable_desc"/></xsl:when>
<xsl:otherwise>
<xsl:value-of select="variable_desc"/>
</xsl:otherwise>
</xsl:choose>
<xsl:variable name="curr_temp_emp_no" select="emp_no"/>
</xsl:for-each>
I am trying to compare variable "curr_temp_emp_no" if new value only it prints "emp_no - variable_desc" otherwise(if same emp_no) then print only "variable_desc".
I understood from google that Variables in XSLT are immutable, once we assign them a value, we can't change them.
Refence: Can you simulate a boolean flag in XSLT?
Can anyone please help me over here in writting this logic.
It looks like you want to compare the last two values. I could achieve this by:
<xsl:template match="/">
<xsl:call-template name="helperTemplate">
<xsl:with-param name="nodes" select="/data/test/loof/super_incompleted"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="helperTemplate">
<xsl:param name="nodes"/>
<xsl:param name="oldVal" select="''"/>
<xsl:if test="$nodes">
<xsl:variable name="curr_temp_emp_no" select="$nodes[1]/emp_no"/>
<xsl:choose>
<xsl:when test="$curr_temp_emp_no != $oldVal">
<xsl:value-of select="$curr_temp_emp_no"/>
<fo:inline> - </fo:inline>
<xsl:value-of select="$nodes[1]/variable_desc"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$nodes[1]/variable_desc"/>
</xsl:otherwise>
</xsl:choose>
<xsl:call-template name="helperTemplate">
<xsl:with-param name="nodes" select="$nodes[position() > 1]"/>
<xsl:with-param name="oldVal" select="$nodes[1]/emp_no"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
If you want to have the comparison on all values(if there was no previous element with that content), you can sort them first and use the same code.
You're asking us to reverse engineer your requirements from non-working code, which is always a challenge, but you seem to be carrying over ideas from procedural programming languages which gives us some clues as to what you imagine you want this code to do.
Basically it looks like a "group-adjacent" problem. In XSLT 2.0 you would do
<xsl:for-each-group select="/data/test/loof/super_incompleted"
group-adjacent="emp_no">
...
</xsl:for-each-group>
If you're stuck with XSLT 1.0 then the usual approach is to process the sequence of nodes with a recursive named template rather than a for-each instruction: you pass the list of nodes as a parameter to the template, together with the current employee id, and then in the template you process the first node in the list, and call yourself recursively to process the remainder of the list.
#ChristianMosz has expanded this suggestion into working code.
Thanks for your reply.
I have used the "preceding-sibling::" which solved my problem. Now the code is something like this.
<xsl:for-each select="/data/test/loof/super_incompleted">
<xsl:variable name="curr_temp_emp_no2" select="emp_no"/>
<xsl:choose>
<xsl:when test="$curr_temp_emp_no2 != preceding-sibling::super_incompleted[1]/emp_no">
<xsl:value-of select="emp_no"/><fo:inline> - </fo:inline><xsl:value-of select="variable_desc"/></xsl:when>
<xsl:otherwise>
<xsl:value-of select="variable_desc"/>
</xsl:otherwise>
</xsl:choose>
<xsl:variable name="curr_temp_emp_no" select="emp_no"/>
</xsl:for-each>
Earlier the table was like this.
1- ABC,1- DFE,1- GFH
2- DFG,2- FGH,2- SDS,2- RTY
Now table looks like this.
1- ABC,DFE,GFH
2- DFG,FGH,SDS

Unable to cast from XRTreeFrag into XNodeSet

I have the following test code... I am trying to pass a node-set as a param. After many hours, i finally was able to pass it to my template.
How I pass my node-set to the template:
<xsl:call-template name="listing">
<xsl:with-param name="customData">
<xsl:apply-templates select="exslt:node-set($data)"/>
</xsl:with-param>
</xsl:call-template>
How my template receives it:
<xsl:template name="listing">
<xsl:param name="customData" select="/.."/>
<xsl:variable name="data">
<xsl:choose>
<xsl:when test="not($customData)">
<xsl:value-of select="/data"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$customData"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<textarea><xsl:copy-of select="$data"></xsl:copy-of></textarea>
</xsl:call-template>
If I set the parameters with a one liner, then it would not complain... example:
<xsl:variable name="data" select="$customData"/>
But as soon as I try to set it like this, it breaks:
<xsl:variable name="data">
<xsl:value-of select="$customData"/>
</xsl:variable>
Getting this error message:
org.apache.xpath.objects.XRTreeFrag cannot be cast to org.apache.xpath.objects.XNodeSet
I was only been able to find another thread dated back in 2000, talk about this similar issue... I need to re-nodeset it back using something like node-set($customData)/* but I tried that, and it was a no go.
EDIT:
OK, I can confirm that I successfully passed the node-set inside my template. But I'm still unable to copy it over to my variable... It kept saying that it is still a RTF.
<xsl:template name="listing">
<xsl:param name="customData" as="node-set"/>
<!--<xsl:variable name="data" select="/data"/>-->
<xsl:variable name="data">
<xsl:choose>
<xsl:when test="count($customData) != 0">
<xsl:copy-of select="$customData"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="/data"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<textarea><xsl:value-of select="$customData/record[1]"/></textarea>
<textarea><xsl:value-of select="/data/record[1]"/></textarea>
<textarea><xsl:value-of select="$data/record[1]"/></textarea>
</xsl:template>
The above test, shows that I can access $customData and the original /data without any problem, they both show the record... but $data is messed up. So that means the copy from $customData to $data wasn't working...
I tried the following ways, none of them work:
<xsl:copy-of select="$customData"/>
<xsl:value-of select="$customData"/>
<xsl:apply-templates select="exslt:node-set($customData)"/>
<xsl:apply-templates select="exslt:node-set($customData)/data"/>
Any idea...?
This error message comes from Xalan, which is an XSLT 1.0 processor. If you are using Xalan, then you are probably using Java, which means there is really no reason at all not to move to XSLT 2.0 in the form of Saxon. You will find that XSLT 2.0 removes many of the restrictions of XSLT 1.0, of which this is one of the most irritating.
If there's a good reason why you can't move forward to XSLT 2.0 (and it's hard to think of one), there's a workaround in the form of the exslt:node-set() function, which converts a result-tree fragment (that is, a variable defined using child instructions) into a document node.
Got it working, basically rather than using apply-template, i need to pass the RTF as a parameter to the template. That is the only way I got it to work.
<xsl:with-param name="data" select="exslt:node-set($customData)"/>
Using this method, I was able to MODIFY data in XSL level. This is really cool, I basically manipulate the data I want, then i reconstruct the root /, and then I pass my customData to my template function.
So rather than reading data off the root, I read my own modified data (constructed inside XSL).
Use of exslt:node-set does indeed suppress the error message org.apache.xpath.objects.XRTreeFrag cannot be cast to org.apache.xpath.objects.XNodeSet
However, the node-set that is created for some reason cannot be used in subsequent XPath expressions; at least it doesn't seem to work with Xalan 2.6.0 / XSLT 1.0 which is the version many people are forced to use for one reason or another.
There is a simple solution: instead of setting the variable to a node-set, set it to the XPath expression instead. Then you can use the dyn:evaluate EXSLT function to evaluate the XPath expression held by the variable.
Your code would look something like this:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dyn="http://exslt.org/dynamic"
extension-element-prefixes="dyn"
exclude-result-prefixes="dyn">
..
<xsl:variable name="data">
<xsl:choose>
<xsl:when test="count(.) != 0">
<xsl:text>.</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>/data</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<textarea>
<xsl:value-of select="dyn:evaluate($data)/record[1]"/>
</textarea>

Using xsl:variable in a xsl:foreach select statement

I'm trying to iterate through an xml document using xsl:foreach but I need the select=" " to be dynamic so I'm using a variable as the source. Here's what I've tried:
...
<xsl:template name="SetDataPath">
<xsl:param name="Type" />
<xsl:variable name="Path_1">/Rating/Path1/*</xsl:variable>
<xsl:variable name="Path_2">/Rating/Path2/*</xsl:variable>
<xsl:if test="$Type='1'">
<xsl:value-of select="$Path_1"/>
</xsl:if>
<xsl:if test="$Type='2'">
<xsl:value-of select="$Path_2"/>
</xsl:if>
<xsl:template>
...
<!-- Set Data Path according to Type -->
<xsl:variable name="DataPath">
<xsl:call-template name="SetDataPath">
<xsl:with-param name="Type" select="/Rating/Type" />
</xsl:call-template>
</xsl:variable>
...
<xsl:for-each select="$DataPath">
...
The foreach threw an error stating: "XslTransformException - To use a result tree fragment in a path expression, first convert it to a node-set using the msxsl:node-set() function."
When I use the msxsl:node-set() function though, my results are blank.
I'm aware that I'm setting $DataPath to a string, but shouldn't the node-set() function be creating a node set from it? Am I missing something? When I don't use a variable:
<xsl:for-each select="/Rating/Path1/*">
I get the proper results.
Here's the XML data file I'm using:
<Rating>
<Type>1</Type>
<Path1>
<sarah>
<dob>1-3-86</dob>
<user>Sarah</user>
</sarah>
<joe>
<dob>11-12-85</dob>
<user>Joe</user>
</joe>
</Path1>
<Path2>
<jeff>
<dob>11-3-84</dob>
<user>Jeff</user>
</jeff>
<shawn>
<dob>3-5-81</dob>
<user>Shawn</user>
</shawn>
</Path2>
</Rating>
My question is simple, how do you run a foreach on 2 different paths?
Try this:
<xsl:for-each select="/Rating[Type='1']/Path1/*
|
/Rating[Type='2']/Path2/*">
Standard XSLT 1.0 does not support dynamic evaluation of xpaths. However, you can achieve your desired result by restructuring your solution to invoke a named template, passing the node set you want to process as a parameter:
<xsl:variable name="Type" select="/Rating/Type"/>
<xsl:choose>
<xsl:when test="$Type='1'">
<xsl:call-template name="DoStuff">
<xsl:with-param name="Input" select="/Rating/Path1/*"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$Type='2'">
<xsl:call-template name="DoStuff">
<xsl:with-param name="Input" select="/Rating/Path2/*"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
...
<xsl:template name="DoStuff">
<xsl:param name="Input"/>
<xsl:for-each select="$Input">
<!-- Do stuff with input -->
</xsl:for-each>
</xsl:template>
The node-set() function you mention can convert result tree fragments into node-sets, that's correct. But: Your XSLT does not produce a result tree fragment.
Your template SetDataPath produces a string, which is then stored into your variable $DataPath. When you do <xsl:for-each select="$DataPath">, the XSLT processor chokes on the fact that DataPath does not contain a node-set, but a string.
Your entire stylesheet seems to be revolve around the idea of dynamically selecting/evaluating XPath expressions. Drop that thought, it is neither possible nor necessary.
Show your XML input and specify the transformation your want to do and I can try to show you a way to do it.