XSLT 1.0 - extract node-set and pass as param - xslt

I am given this XML and have to render quite a bit from it and most is working fine, but I am stomped trying to extract the node-set of color whos key matches the key of the bar element and the attribute is a hardcoded string ('data' in this case). The node-set is to be passed as parameter to a template and each color line must only appear once:
<report>
<settings>
<colors>
<color key="1-1" name="frame" value="..." ... />
<color key="1-1" name="data" value="..." ... />
<color key="2-1" name="frame" value="..." ... />
<color key="2-1" name="data" value="..." ... />
<color key="3-1" name="frame" value="..." ... />
<color key="3-1" name="data" value="..." ... />
</colors>
<comp>
<cont>
<bar key="1-1" .../>
<bar key="1-1" .../>
<bar key="2-1" .../>
</cont>
<comp>
<!-- possibly more <comp/cont/bar> below that may not be mixed with the above -->
</settings>
</report>
In my XSLT file I have this (extract):
<xsl:key name="barnode" match="bar" use="#key"/>
<xsl:key name="colorlookup" match="/report/settings/colors/color" use="#key"/>
<!-- this runs at the `cont` element level, i.e. `bar` can be accessed without prefix -->
<!-- set $x to the node-list of bars with unique #key attribute -->
<xsl:call-template name="renderit">
<xsl:with-param name="colors">
<!-- 'bars' contains node-set of 'bar' elements with #key being unique -->
<xsl:variable name="bars" select="bar[generate-id() = generate-id(key('barnode', #key)[1])]"/>
<xsl:for-each select="$bars">
<xsl:value-of select="key('colorlookup', #key)[#name='data']"/>
</xsl:for-each>
</xsl:with-param>
</xsl:call-template>
The problem is, that this does not pass a node-set, but a tree-fragment. Is it possible to make a select that does the same as above, but returns a node-set?
Edit:
Expected node-set:
<color key="1-1" name="data" value="..." ... />
<color key="2-1" name="data" value="..." ... />
I am not sure if the presented XSLT will even generate this result tree fragment as I don't know how to print it (for debug purposes).

Try
<xsl:with-param name="colors" select="key('colorlookup', bar[generate-id() = generate-id(key('barnode', #key)[1])]/#key)[#name = 'data']"/>

Related

XSLT: merge two tags using attribute from one, tag from another with a transform/mapping

I have two tags in the input file, variable and type:
<variable baseType="int" name="X">
</variable>
<type baseType="structure" name="Y">
<variableInstance name="X" />
</type>
And I need to generate the following output file:
<Item name="Y">
<Field name="X" type="Long" />
</Item>
So conceptually my approach here has been to convert the type tag into the Item tage, the variable instance to the Field. That's working fine:
<xsl:for-each select="type[#baseType='structure']">
<Item>
<xsl:attribute name="name"><xsl:value-of select="#name" /></xsl:attribute>
<xsl:for-each select="variableInstance">
<Field>
<xsl:attribute name="name"><xsl:value-of select="#name" /></xsl:attribute>
<xsl:attribute name="type">**THIS IS WHERE I'M STUCK**</xsl:attribute>
</Field>
</xsl:for-each>
</Item>
</xsl:for-each>
The problem I'm stuck on is:
I don't know how to get the variableInstance/Field tag to match on the variable tag by name, so I can access the baseType.
I need to map "int" to "Long" once I'm able to do 1.
Thanks in advance!
PROBLEM 1.
For the first problem that you have you can use a key:
<xsl:key name="variable-key" match="//variable" use="#name" />
That key is going to index all variable elements in the document, using their name. So now, we can access any of those elements by using the following XPath expression:
key('variable-key', 'X')
Using this approach is efficient when you have a lot of variable elements.
NOTE: this approach is not valid if each variable has its own scope (i.e. you have local variables which are not visible in different parts of the document). In that case this approach should be modified.
PROBLEM 2.
For mapping attributes you could use a template like the following:
<xsl:template match="#baseType[. = 'int']">
<xsl:attribute name="baseType">
<xsl:value-of select="'Long'" />
</xsl:attribute>
</xsl:template>
The meaning of this transformation is: each time that we match a baseType attribute with int value, it has to be replaced by a Long value.
This transformation would be in place for each #baseType attribute in the document.
Using the described strategies a solution could be:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<!-- Index all variable elements in the document by name -->
<xsl:key name="variable-key"
match="//variable"
use="#name" />
<!-- Just for demo -->
<xsl:template match="text()" />
<!-- Identity template: copy attributes by default -->
<xsl:template match="#*">
<xsl:copy>
<xsl:value-of select="." />
</xsl:copy>
</xsl:template>
<!-- Match the structure type -->
<xsl:template match="type[#baseType='structure']">
<Item>
<xsl:apply-templates select="*|#*" />
</Item>
</xsl:template>
<!-- Match the variable instance -->
<xsl:template match="variableInstance">
<Field>
<!-- Use the key to find the variable with the current name -->
<xsl:apply-templates select="#*|key('variable-key', #name)/#baseType" />
</Field>
</xsl:template>
<!-- Ignore attributes with baseType = 'structure' -->
<xsl:template match="#baseType[. = 'structure']" />
<!-- Change all baseType attributes with long values to an attribute
with the same name but with an int value -->
<xsl:template match="#baseType[. = 'int']">
<xsl:attribute name="baseType">
<xsl:value-of select="'Long'" />
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
That code is going to transform the following XML document:
<!-- The code element is present just for demo -->
<code>
<variable baseType="int" name="X" />
<type baseType="structure" name="Y">
<variableInstance name="X" />
</type>
</code>
into
<Item name="Y">
<Field baseType="Long" name="X"/>
</Item>
Oki I've got a solution for point 1 xsltcake slice or with use of templates. For point two I would probably use similar template to the one that Pablo Pozo used in his answer

Use XSLT key() function to lookup nodes based on two attributes in separate elements

I'm trying to use the XSLT key() function to return all the <Code> elements in an XML file that match the following two criteria:
Code[code=$code] AND ancestor::CodeType[type=$codeType]`
Here is a simplified example of what the input XML looks like:
<Items>
<Item code-number="C1" category="ABC" />
<Item code-number="C3" category="ABC" />
<Item code-number="C1" category="XYZ" />
</Items>
<CodeTypes>
<CodeType type="ABC">
<SubType title="Category III Codes"> <!-- <SubType> elements are optional -->
<SubType title="Subcategory III-15 Codes">
<Code code="C1" description="Red" />
<Code code="C2" description="Green" />
<Code code="C3" description="Blue" />
<Code code="C3" description="Purple" /> <!-- Same code can appear more than once -->
</SubType>
</SubType>
<CodeType>
<CodeType type="XYZ">
<Code code="C1" description="Black" /> <!-- Same code can be used for multiple CodeTypes -->
<Code code="C2" description="Orange" />
<Code code="C3" description="Yellow" />
<CodeType>
</CodeTypes>
Note that the comments aren't actually there in the actual XML, I'm just adding them here to clarify the XML structure.
Here is the XSLT transform I am trying to use, though it doesn't seem to be working:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="LookupMatchingCodeElements" match="Code" use="concat(../#code, '+', ancestor::CodeType/#type)" />
<xsl:template match="Item">
<xsl:call-template name="GetCodeElements">
<xsl:with-param name="code" select="#code-number" />
<xsl:with-param name="codeType" select="#category" />
</xsl:call-template>
</xsl:template>
<xsl:template name="GetCodeElements">
<xsl:param name="code" />
<xsl:param name="codeType" />
<xsl:for-each select="key('LookupMatchingCodeElements', concat($code, '+', $codeType))">
<!-- process each <Code> element -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
And this is what I want the key() function to return with different inputs:
<!-- For code="C1" AND codeType="ABC" -->
<Code code="C1" description="Red" />
<!-- For code="C3" AND codeType="ABC" -->
<Code code="C3" description="Blue" />
<Code code="C3" description="Purple" />
<!-- For code="C1" AND codeType="XYZ" -->
<Code code="C1" description="Black" />
Is this possible with the key() function? As there are hundreds of thousands of both <Item> and <Code> elements, being able to use <xsl:key> is very important.
Use:
<xsl:key name="kCode" match="Code"
use="concat(ancestor::CodeType[1]/#type, '+', #code)"/>
Here is a complete transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kCode" match="Code"
use="concat(ancestor::CodeType[1]/#type, '+', #code)"/>
<xsl:template match="/">
<xsl:copy-of select="key('kCode', 'ABC+C1')"/>
====================
<xsl:copy-of select="key('kCode', 'ABC+C3')"/>
====================
<xsl:copy-of select="key('kCode', 'XYZ+C1')"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following document (the provided XML text -- corrected malformedness):
<t>
<Items>
<Item code-number="C1" category="ABC" />
<Item code-number="C3" category="ABC" />
<Item code-number="C1" category="XYZ" />
</Items>
<CodeTypes>
<CodeType type="ABC">
<SubType title="Category III Codes">
<!-- <SubType> elements are optional -->
<SubType title="Subcategory III-15 Codes">
<Code code="C1" description="Red" />
<Code code="C2" description="Green" />
<Code code="C3" description="Blue" />
<Code code="C3" description="Purple" />
<!-- Same code can appear more than once -->
</SubType>
</SubType>
</CodeType>
<CodeType type="XYZ">
<Code code="C1" description="Black" />
<!-- Same code can be used for multiple CodeTypes -->
<Code code="C2" description="Orange" />
<Code code="C3" description="Yellow" />
</CodeType>
</CodeTypes>
</t>
the wanted, correct result is produced:
<Code code="C1" description="Red"/>
====================
<Code code="C3" description="Blue"/>
<Code code="C3" description="Purple"/>
====================
<Code code="C1" description="Black"/>

XSLT nested lookup

New to XSLT, but have been learning a lot from posts here. However, I'm stuck on one problem.
I am using XSLT to create a report for a device installation. The input XML looks like this:
<DeviceTypes>
<DeviceInfo Model="51473">
<Channels>
<ChannelInfo ChannelId="1" IsImplemented="false" SampRateHardware="448" />
<ChannelInfo ChannelId="2" IsImplemented="true" SampRateHardware="224" />
</Channels>
</DeviceInfo>
<DeviceInfo Model="51474">
<Channels>
<ChannelInfo ChannelId="1" IsImplemented="true" SampRateHardware="448" />
<ChannelInfo ChannelId="2" IsImplemented="true" SampRateHardware="224" />
</Channels>
</DeviceInfo>
</DeviceTypes>
<Installation>
<InstalledDevice Serial="597657" Model="51473">
<Channels>
<InstalledChannel ChannelId="1" Name="foo" />
<InstalledChannel ChannelId="2" Name="bar" />
</Channels>
</InstalledDevice>
</Installation>
I want to only process the InstallChannel node if the corresponding ChannelInfo has an "IsImplemented" set to true. By "corresponding" I mean I am looking for the ChannelInfo with the same ChannelId and the same Model under the parent node. Note that channels with the same ChannelId may have different IsImplemented values depending on what device they are under.
I've been using and the key() function to successfully lookup, but this nested lookup has me stumped.
Thanks,
-Mat
Here is a short and simple (no conditionals, no variables no xsl:for-each) solution using keys:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kCI-ByIdImpl" match="ChannelInfo"
use="concat(#ChannelId,
'+', #IsImplemented,
'+', ../../#Model)"/>
<xsl:template match="/*">
<xsl:copy-of select=
"Installation/*/*
/InstalledChannel
[key('kCI-ByIdImpl',
concat(#ChannelId, '+true',
'+', ../../#Model)
)
]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML fragment (wrapped into a single top element to be made a well-formed XML document):
<t>
<DeviceTypes>
<DeviceInfo Model="51473">
<Channels>
<ChannelInfo ChannelId="1" IsImplemented="false" SampRateHardware="448" />
<ChannelInfo ChannelId="2" IsImplemented="true" SampRateHardware="224" />
</Channels>
</DeviceInfo>
<DeviceInfo Model="51474">
<Channels>
<ChannelInfo ChannelId="1" IsImplemented="true" SampRateHardware="448" />
<ChannelInfo ChannelId="2" IsImplemented="true" SampRateHardware="224" />
</Channels>
</DeviceInfo>
</DeviceTypes>
<Installation>
<InstalledDevice Serial="597657" Model="51473">
<Channels>
<InstalledChannel ChannelId="1" Name="foo" />
<InstalledChannel ChannelId="2" Name="bar" />
</Channels>
</InstalledDevice>
</Installation>
</t>
only the wanted InstalledChannel element is processed (in this case simply copied to the output):
<InstalledChannel ChannelId="2" Name="bar"/>
Explanation: Appropriate use of a composite key.
I believe that using the templates makes for better readability/extendability: The key is using the variable to be able to reference both the Model and the ChannelId for the ChannelInfo node in the xpath for the InstalledChannel, so start with the InstalledDevice, and work your way down the heirarchy
<xsl:apply-templates select="//InstalledDevice"/>
<xsl:template match="//InstalledDevice">
<xsl:variable name="model">
<xsl:value-of select="#Model"/>
</xsl:variable>
<xsl:for-each select="Channels/InstalledChannel">
<xsl:variable name="channelId">
<xsl:value-of select="#ChannelId"/>
</xsl:variable>
<xsl:if test="//DeviceInfo[#Model=$model]/Channels/ChannelInfo[#ChannelId=$channelId and #IsImplemented='true']">
Processing Goes Here
</xsl:if>
</xsl:for-each>
</xsl:template>
So that we can preserve context of our model variable, I moved the InstalledChannel processing into the same template, and added the for-each. That way each InstalledChannel instance can be examined individually for whether it needs to be processed, and handled accordingly.
Something like this should work.
/Installation/InstalledDevice/Channels/InstalledChannel/[count(/DeviceTypes/DeviceInfo/Channels/ChannelInfo[#ChannelId = #ChannelId and #IsImplemented = 'true') = 1]

XSL : how to select a unique nodes in a nodeset

I have been reading on the different question talking about selecting unique nodes in a document (using the Muenchian method) but in my case I cannot use keys (or I do not know how) because I am working on a node set and not on the document.
And keys cannot be set on a node-set. Basically I have a variable:
<xsl:variable name="limitedSet" select="
$deviceInstanceNodeSet[position() <= $tableMaxCol]"
/>
which contains <deviceInstance> nodes which themselves containing <structure> elements
the node set may be represented this way:
<deviceInstance name="Demux TSchannel" deviceIndex="0">
<structure name="DemuxTschannelCaps">
</structure>
</deviceInstance>
<deviceInstance name="Demux TSchannel" deviceIndex="1">
<structure name="DemuxTschannelCaps">
</structure>
</deviceInstance>
<deviceInstance name="Demux TSchannel" deviceIndex="3">
<structure name="otherCaps">
</structure>
</deviceInstance>
And I do not know a to select <structure> elements that only have different name. The select would in this example return two <structure> elements, being:
<structure name="DemuxTschannelCaps"></structure>
<structure name="otherCaps"></structure>
I have tried
select="$limitedSet//structure[not(#name=preceding::structure/#name)]"
but the preceding axis goes all along the document and not the $limitedSet?
I am stuck, can someone help me. Thank you.
<xsl:variable name="structure" select="$limitedSet//structure" />
<xsl:for-each select="$structure">
<xsl:variable name="name" select="#name" />
<xsl:if test="generate-id() = generate-id($structure[#name = $name][1])">
<xsl:copy-of select="." />
</xsl:if>
</xsl:for-each>
This could be aided by a key:
<xsl:key name="kStructureByName" match="structure" use="#name" />
<!-- ... -->
<xsl:if test="generate-id() = generate-id(key('kStructureByName', $name)[1])">
Depending on your input, the key would have to capture some additional contextual information:
<xsl:key name="kStructureByName" match="structure" use="
concat(ancestor::device[1]/#id, ',', #name)
" />
<!-- ... -->
<xsl:variable name="name" select="concat(ancestor::device[1]/#id, ',', #name)" />
<xsl:if test="generate-id() = generate-id(key('kStructureByName', $name)[1])">
select="$limitedSet//structure[not(#name=preceding::structure[count($limitedSet) = count($limitedSet | ..)]/#name)]"

Optional parameters when calling an XSL template

Is there way to call an XSL template with optional parameters?
For example:
<xsl:call-template name="test">
<xsl:with-param name="foo" select="'fooValue'" />
<xsl:with-param name="bar" select="'barValue'" />
</xsl:call-template>
And the resulting template definition:
<xsl:template name="foo">
<xsl:param name="foo" select="$foo" />
<xsl:param name="bar" select="$bar" />
<xsl:param name="baz" select="$baz" />
...possibly more params...
</xsl:template>
This code will gives me an error "Expression error: variable 'baz' not found." Is it possible to leave out the "baz" declaration?
Thank you,
Henry
You're using the xsl:param syntax wrong.
Do this instead:
<xsl:template name="foo">
<xsl:param name="foo" />
<xsl:param name="bar" />
<xsl:param name="baz" select="DEFAULT_VALUE" />
...possibly more params...
</xsl:template>
Param takes the value of the parameter passed using the xsl:with-param that matches the name of the xsl:param statement. If none is provided it takes the value of the select attribute full XPath.
More details can be found on W3School's entry on param.
Personally, I prefer doing the following:
<xsl:call-template name="test">
<xsl:with-param name="foo">
<xsl:text>fooValue</xsl:text>
</xsl:with-param>
I like using explicitly with text so that I can use XPath on my XSL to do searches. It has come in handy many times when doing analysis on an XSL I didn't write or didn't remember.
The value in the select part of the param element will be used if you don't pass a parameter.
You are getting an error because the variable or parameter $baz does not exist yet. It would have to be defined at the top level for it to work in your example, which is not what you wanted anyway.
Also if you are passing a literal value to a template then you should pass it like this.
<xsl:call-template name="test">
<xsl:with-param name="foo">fooValue</xsl:with-param>
Please do not use <xsl:param .../> if you do not need it to increase readability.
This works great:
<xsl:template name="inner">
<xsl:value-of select="$message" />
</xsl:template>
<xsl:template name="outer">
<xsl:call-template name="inner">
<xsl:with-param name="message" select="'Welcome'" />
</xsl:call-template>
</xsl:template>