XSLT nested lookup - xslt

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]

Related

xslt to group xml based on element values

want to group request based on similar package id using manual XSLT
Here is input xml which contains plan_id on which it needs to be grouped
<subscriptions>
<package>
<package_plan>
<plan_id>1111</plan_id>
<plan_name>economy1</plan_name>
</package_plan>
<rate_channel>
<rateid>1F1</rateid>
<currency>USD</currency>
<package_duration>monthly</package_duration>
<price>3</price>
</rate_channel>
</package>
<package>
<package_plan>
<plan_id>1111</plan_id>
<plan_name>economy1</plan_name>
</package_plan>
<rate_channel>
<rateid>1F2</rateid>
<currency>USD</currency>
<package_duration>quaterly</package_duration>
<price>11</price>
</rate_channel>
</package>
<package>
<package_plan>
<plan_id>2222</plan_id>
<plan_name>economy2</plan_name>
</package_plan>
<rate_channel>
<rateid>1F3</rateid>
<currency>INR</currency>
<package_duration>monthly</package_duration>
<price>250</price>
</rate_channel>
</package>
</subscriptions>
Looking for output as:
<subscriptions>
<package>
<plan_id>1111</plan_id>
<plan_name>economy1</plan_name>
<channels>
<rate_channel>
<rateid>1F1</rateid>
<currency>USD</currency>
<package_duration>monthly</package_duration>
<price>3</price>
</rate_channel>
<rate_channel>
<rateid>1F2</rateid>
<currency>USD</currency>
<package_duration>quaterly</package_duration>
<price>11</price>
</rate_channel>
</channels>
</package>
<package>
<plan_id>2222</plan_id>
<plan_name>economy2</plan_name>
<channels>
<rate_channel>
<rateid>1F3</rateid>
<currency>INR</currency>
<package_duration>monthly</package_duration>
<price>250</price>
</rate_channel>
</channels>
</package>
</subscriptions>
I've been doing some search and reading the other posts, and I don't think they cover exactly what I want to do
Thanks in advance...
Below are the XSLT 1.0 and XSLT 2.0 solutions. Both the solutions use a different approach as rightly pointed by #Tim C in the comment.
XSLT 1.0
In case of XSLT 1.0, muenchian grouping is used. A <xsl:key> needs to be defined to group the elements. In this case, <plan_id> is the key on which the grouping will be done.
<xsl:key name="plan" match="package" use="package_plan/plan_id" />
The package elements are matched accordingly and the grouped data is copied to the output.
<xsl:template match="package[generate-id() = generate-id(key('plan', package_plan/plan_id)[1])]">
<xsl:copy>
<xsl:variable name="varPlan" select="key('plan', package_plan/plan_id)" />
<xsl:apply-templates select="$varPlan[1]/package_plan/plan_id" />
<xsl:apply-templates select="$varPlan[1]/package_plan/plan_name" />
<channels>
<xsl:for-each select="$varPlan">
<xsl:apply-templates select="rate_channel" />
</xsl:for-each>
</channels>
</xsl:copy>
</xsl:template>
The rest of the <package> elements are removed.
<xsl:template match="package" />
XSLT 2.0
In XSLT 2.0, <xsl-for-each-group> feature is available especially for grouping of elements. In this case, using this feature and the current-group() function, grouping can be achieved.
<xsl:template match="subscriptions">
<xsl:copy>
<xsl:for-each-group select="package" group-by="package_plan/plan_id">
<package>
<xsl:apply-templates select="current-group()[1]/package_plan/plan_id" />
<xsl:apply-templates select="current-group()[1]/package_plan/plan_name" />
<channels>
<xsl:for-each select="current-group()">
<xsl:apply-templates select="rate_channel" />
</xsl:for-each>
</channels>
</package>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
In both the above cases, an identity transform template should be used to copy the data as is to the output.
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()" />
</xsl:copy>
</xsl:template>

XSLT grouping with filter

I am trying to do XSLT grouping within a call template while applying a filter. The below code works fine but it is not considering the filter condition and is returning all the nodes . Please help me to understand what I am doing wrong . My understanding is that is that it has something to do with the scope of the below line
<xsl:for-each select="key('ParentName',#ParentName)" >
Here is the xml and xslt .
<?xml version="1.0" encoding="utf-16"?>
<Documentation>
<Consequences>
<Nodes>
<Node1 name ="abc1" ParentName="Group1" IsInternal ="true" />
<Node1 name ="abc2" ParentName="Group2" IsInternal ="true"/>
<Node1 name ="bcd1" ParentName="Group2" IsInternal ="true" />
<Node1 name ="bcd2" ParentName="Group1" IsInternal ="false"/>
<Node1 name ="efg1" ParentName="Group2" IsInternal ="false"/>
<Node1 name ="efg2" ParentName="Group1" IsInternal ="false" />
</Nodes>
</Consequences>
</Documentation>
XSLT : -
<xsl:output method="xml" indent="yes"/>
<xsl:template match="Consequences" >
<xsl:call-template name="Template1">
<xsl:with-param name="NodeList" select="Nodes/Node1[#IsInternal='true']"/>
</xsl:call-template>
</xsl:template>
<xsl:key name="ParentName" match="Node1" use="#ParentName" />
<xsl:template name ="Template1">
<xsl:param name="NodeList" />
<xsl:for-each select="$NodeList[generate-id()=generate-id(key('ParentName',#ParentName)[1])]">
<Group>
<xsl:value-of select="#ParentName" />
<xsl:for-each select="key('ParentName',#ParentName)" >
<SubItems>
<xsl:value-of select="#name" />
</SubItems>
</xsl:for-each>
</Group>
</xsl:for-each>
</xsl:template>
Assuming that you are trying to select only the elements who's #IsInternal='true', instead of simply all of the Node1 elements that have the specified #ParenName.
Your xsl:key is matching on all of the Node1 elements and indexed by the #ParentName value. If you are only using that key to retrieve the elements who's #IsInternal='true', then you can add a predicate filter to the match criteria:
<xsl:key name="ParentName" match="Node1[#IsInternal='true']" use="#ParentName" />
If you need to use that key to lookup Node1 elements by #ParentName in other areas without regard to what the #IsInternal value is, then you could filter the items returned from the key lookup:
<xsl:for-each select="key('ParentName',#ParentName)[#IsInternal='true']" >
<SubItems>
<xsl:value-of select="#name" />
</SubItems>
</xsl:for-each>
You could also define a key that uses the value of both the #ParentName and the #IsInternal values as the key value:
<xsl:key name="ParentName" match="Node1"
use="concat(#ParentName,'-',#IsInternal)" />
and then retrieve them with the same key value:
key('ParentName',concat(#ParentName,'-',#IsInternal))

Removing duplicates from a bag returned by xsl:key based on attribute value

I have the following xml file.
<Bank>
<Person personId="1" type="1071" deleted="0">
</Person>
<Person personId="2" type="1071" deleted="0">
</Person>
<Person personId="3" type="1071" deleted="0">
</Person>
<Account>
<Role personId="1" type="1025" />
</Account>
<Account>
<Role personId="1" type="1025" />
</Account>
<Account>
<Role personId="1" type="1018" />
</Account>
<Account>
<Role personId="3" type="1025" />
<Role personId="1" type="1018" />
</Account>
<Account>
<Role personId="2" type="1025" />
</Account>
</Bank>
and the following xsl transformation.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="ISO-8859-1" />
<xsl:strip-space elements="*" />
<xsl:key name="roleKey"
match="Role[(#type = '1025' or #type = '1018' or #type = '1022' or #type = '1023') and not(#validTo)]"
use="#personId" />
<xsl:template match="Person">
<xsl:value-of select="#personId" />
<xsl:variable name="roles" select="key('roleKey', #personId)" />
<xsl:for-each select="$roles">
<xsl:text>;</xsl:text><xsl:value-of select="#type" />
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
The actual result is following and I would like to remove the duplicated type values.
1;1025;1025;1018;1018
2;1025
3;1025
The expected results should be like follows...
1;1025;1018
2;1025
3;1025
I have tried the tips involving keyword following from this website and also the trick with the Muenchian method. They all do not work as they seem to be browsing through the whole document and matching the duplicates for each and every Person element, whereas I want to remove duplicates only in a Person context defined by personId attribute.
How do I remove those duplicates from the bag returned by key function? Or maybe there is a method that I can use in xsl:for-each to print only what I want to?
I can only use what there is available in XSLT 1.0. I do not have a possibility to use any XSLT 2.0 processor.
Ok, do not know if this is the best solution available but I achieved what I wanted by introducing such a key and using Muenchian method.
<xsl:key name="typeKey" match="Role" use="concat(#type, '|', #personId)" />
The whole transformation looks like that after the change...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="ISO-8859-1" />
<xsl:strip-space elements="*" />
<xsl:key name="roleKey"
match="Role[(#type = '1025' or #type = '1018' or #type = '1022' or #type = '1023') and not(#validTo)]"
use="#personId" />
<xsl:key name="typeKey" match="Role" use="concat(#type, '|', #personId)" />
<xsl:template match="Person">
<xsl:value-of select="#personId" />
<xsl:variable name="roles" select="key('roleKey', #personId)" />
<xsl:for-each select="$roles[generate-id() = generate-id(key('typeKey', concat(#type, '|', #personId)))]">
<xsl:text>;</xsl:text><xsl:value-of select="#type" />
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
And the actual result is now...
1;1025;1018
2;1025
3;1025

how to transform xpath expression to xsl key?

I'm stuck with what should be a simple problem with XSLT keys.
If it is relevant, I'm forced into an XSLT 1.0 parser.
Sample XML:
<updates>
<update>
<id>first</id>
<pkglist>
<collection arch='i686'>
<package name='bin' version='1.0'/>
</collection>
<collection arch='x86_64'>
<package name='bin' version='1.0'/>
</collection>
</pkglist>
</update>
<update>
<id>second</id>
<pkglist>
<collection arch='i686'>
<package name='bin' version='1.1'/>
<package name='conf' version='1.1'/>
</collection>
<collection arch='x86_64'>
<package name='bin' version='1.2'/>
<package name='conf' version='1.1'/>
</collection>
</pkglist>
</update>
<update>
<id>third</id>
<pkglist>
<collection arch='i686'>
<package name='bin' version='1.3'/>
<package name='conf' version='1.1'/>
<package name='src' version='1.3'/>
</collection>
<collection arch='x86_64'>
<package name='bin' version='1.3'/>
<package name='conf' version='1.2'/>
<package name='src' version='1.3'/>
</collection>
</pkglist>
</update>
</updates>
This XPATH selects what I'm looking for
/updates//update[pkglist/collection/package/#name = 'bin']/id/text()
I'm looking for all 'id' values in the whole document that have packages with an attribute of 'name'. But in the real world I have way more packages than would be sensible to list out by hand.
So I figured a key would be the way to go
<xsl:key name="idByPackage" match="/updates//update/pkglist/collection/package/#name" use="../../../../id" />
But that doesn't give me back anything useful
<xsl:key name="idByPackage" match="/updates//update/pkglist/collection/package/#name" use="../../../../id" />
<xsl:template match="/">
<xsl:apply-templates select="updates/update" />
</xsl:template>
<xsl:template match="update">
Updates:
<xsl:value-of select="id" />
Related updates
<xsl:apply-templates select="pkglist" />
</xsl:template>
<xsl:template match="pkglist">
<xsl:for-each select="key('idByPackage', collection/package/#name)">
<xsl:value-of select="." />
<xsl:value-of select="collection/package/#name" />
</xsl:for-each>
</xsl:template>
I know I'm in the right area as when I change the key to this:
<xsl:key name="idByPackage" match="/updates//update/pkglist/collection/package/#name" use="../../../../pkglist/collection/package/#name" />
The same xsl template spits pack my package names.
When I run the template with the 'looks valid to me but does not work' key I get this:
Updates:
first
Related updates
Updates:
second
Related updates
Updates: third
Related updates
When I expect to get something like
Updates:
first
Related updates
second
third
Updates:
second
Related updates
first
third
Updates: third
Related updates
first
second
What am I doing wrong?
This is the key you need:
<xsl:key name="idByPackage"
match="update/id" use="../pkglist/collection/package/#name" />
When this XSLT is run on your sample input:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes" omit-xml-declaration="yes"/>
<xsl:key name="idByPackage"
match="update/id" use="../pkglist/collection/package/#name" />
<xsl:variable name="nl" select="'
'" />
<xsl:template match="/">
<xsl:apply-templates select="updates/update" />
</xsl:template>
<xsl:template match="update">
<xsl:value-of select="concat('Updates:', $nl,
id, $nl,
'Related updates:', $nl)" />
<xsl:variable name="name" select="pkglist/collection/package/#name" />
<xsl:apply-templates select="key('idByPackage', $name)[. != current()/id]" />
</xsl:template>
<xsl:template match="id">
<xsl:value-of select="concat(., $nl)" />
</xsl:template>
</xsl:stylesheet>
The result is:
Updates:
first
Related updates:
second
third
Updates:
second
Related updates:
first
third
Updates:
third
Related updates:
first
second

How can I generate basic documentation from a Relax NG Schema

I'm trying to generate very simple documentation from the annotations in a Relax NG XML Schema. For example, given the following Relax NG:
<?xml version="1.0" encoding="UTF-8"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0" xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<start>
<element name="topNode">
<ref name="topNode-ref"/>
</element>
</start>
<define name="topNode-ref">
<a:documentation>This is the top of the doc.</a:documentation>
<oneOrMore>
<element name="level1">
<ref name="level1-ref"/>
</element>
</oneOrMore>
</define>
<define name="level1-ref">
<a:documentation>Here's some notes about level1.</a:documentation>
<attribute name="att1">
<a:documentation>Details about att1.</a:documentation>
</attribute>
<element name="subLevel2">
<ref name="subLevel2-ref"/>
</element>
</define>
<define name="subLevel2-ref">
<a:documentation>Notes on subLevel2.</a:documentation>
<attribute name="subAtt"/>
<zeroOrMore>
<element name="subLevel3">
<ref name="subLevel3-ref"/>
</element>
</zeroOrMore>
</define>
<define name="subLevel3-ref">
<a:documentation>And here is subLevel3.</a:documentation>
<attribute name="subSubAtt"/>
</define>
</grammar>
Which would be used to valid an XML file like:
<?xml version="1.0" encoding="UTF-8"?>
<topNode>
<level1 att1="some test">
<subLevel2 subAtt="more text"></subLevel2>
</level1>
<level1 att1="quick">
<subLevel2 subAtt="brown">
<subLevel3 subSubAtt="fox"></subLevel3>
</subLevel2>
</level1>
</topNode>
I'd like to be able to produce documentation that lists the basic XPath to each element/attribute and then display any corresponding documentation annotations. For example:
/topNode
This is the top of the doc.
/topNode/level1
Here's some notes about level1
/topNode/level1/#att1
Details about att1.
etc...
Eventually, I'll add in more documentation about "zeroOrMore", possible data types, etc... but I need to get this first step solved first.
I've found the Techquila RELAX-NG Documentation Tools. I've played around with the rng to docbook stylesheet, but it don't do what I'm looking for. It just lists elements individually with no details about the XPath as far as I can tell. I don't see how I can use it as a starting point to get the output I'm after.
Is it possible (and if so, how?) to produce this type of documentation output with XSLT given the RelaxNG example provided?
While XSLT would be ideal, it's not a requirement. I'm open for anything that gets the job done.
This will work for a very simple grammar like your example.
<?xml version='1.0'?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:r="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0"
>
<xsl:output method="text" />
<xsl:template match="/">
<xsl:apply-templates select="//r:define[a:documentation] | //r:attribute[a:documentation]" />
</xsl:template>
<xsl:template match="r:define">
<xsl:variable name="doc" select="a:documentation" />
<xsl:call-template name="print-path">
<xsl:with-param name="elm" select="//r:element[r:ref/#name=current()/#name]" />
</xsl:call-template>
<xsl:value-of select="$doc" /><xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="r:attribute">
<xsl:variable name="doc" select="a:documentation" />
<xsl:call-template name="print-path">
<xsl:with-param name="elm" select="//r:element[r:ref/#name=current()/ancestor::r:define/#name]" />
<xsl:with-param name="path" select="concat('/#',#name)" />
</xsl:call-template>
<xsl:value-of select="$doc" /><xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="print-path">
<xsl:param name="elm" />
<xsl:param name="path" />
<xsl:variable name="parent" select="//r:ref[#name=$elm/ancestor::r:define/#name]/ancestor::r:element" />
<xsl:message><xsl:value-of select="$elm/#name" /></xsl:message>
<xsl:choose>
<xsl:when test="$parent">
<xsl:call-template name="print-path">
<xsl:with-param name="elm" select="$parent" />
<xsl:with-param name="path" select="concat('/',$elm/#name,$path)" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('/',$elm/#name,$path)" /><xsl:text>
</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>