xslt gmuenchian grouping with sub-groups - xslt

I am trying to re-structure xml data into groups and sub-group. I was able to get it to work, but my code has to include something, that looks (at least to me) like a workaround. Here are my sample files:
Data.xml:
<data>
<record Group="g1" SubGroup="sg1">Record 1</record>
<record Group="g2" SubGroup="sg1">Record 2</record>
<record Group="g1" SubGroup="sg1">Record 3</record>
<record Group="g2" SubGroup="sg1">Record 4</record>
<record Group="g2" SubGroup="sg2">Record 5</record>
<record Group="g1" SubGroup="sg2">Record 6</record>
</data>
Stylesheet.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" version="1.0" indent="yes" encoding="UTF-8"/>
<xsl:key name="Group" match="record" use="#Group" />
<xsl:key name="SubGroup" match="record" use="#SubGroup" />
<xsl:template match="/data">
<xsl:variable name="Records" select="record"/>
<data>
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('Group',#Group)[1])]">
<xsl:sort select="#Group"/>
<xsl:variable name="Group" select="#Group"/>
<xsl:call-template name="Group">
<xsl:with-param name="Records" select="$Records[#Group = $Group]"/>
<xsl:with-param name="Group" select="$Group"/>
</xsl:call-template>
</xsl:for-each>
</data>
</xsl:template>
<xsl:template name="Group">
<xsl:param name="Records"/>
<xsl:param name="Group"/>
<group name="{$Group}">
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',#SubGroup)[1])]">
<!-- this works: <xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',#SubGroup)[#Group = $Group][1])]"> -->
<xsl:sort select="#SubGroup"/>
<xsl:variable name="SubGroup" select="#SubGroup"/>
<xsl:call-template name="SubGroup">
<xsl:with-param name="Records" select="$Records[#SubGroup = $SubGroup]"/>
<xsl:with-param name="Group" select="$Group"/>
<xsl:with-param name="SubGroup" select="$SubGroup"/>
</xsl:call-template>
</xsl:for-each>
</group>
</xsl:template>
<xsl:template name="SubGroup">
<xsl:param name="Records"/>
<xsl:param name="Group"/>
<xsl:param name="SubGroup"/>
<subgroup name="{$SubGroup}">
<xsl:for-each select="$Records">
<xsl:copy-of select="."/>
</xsl:for-each>
</subgroup>
</xsl:template>
</xsl:stylesheet>
This is the output generated:
<?xml version="1.0" encoding="UTF-8"?>
<data>
<group name="g1">
<subgroup name="sg1">
<record Group="g1" SubGroup="sg1">Record 1</record>
<record Group="g1" SubGroup="sg1">Record 3</record>
</subgroup>
</group>
<group name="g2">
<subgroup name="sg2">
<record Group="g2" SubGroup="sg2">Record 5</record>
</subgroup>
</group>
</data>
but this is the output, I want to have:
<?xml version="1.0" encoding="UTF-8"?>
<data>
<group name="g1">
<subgroup name="sg1">
<record Group="g1" SubGroup="sg1">Record 1</record>
<record Group="g1" SubGroup="sg1">Record 3</record>
</subgroup>
<subgroup name="sg2">
<record Group="g1" SubGroup="sg2">Record 6</record>
</subgroup>
</group>
<group name="g2">
<subgroup name="sg1">
<record Group="g2" SubGroup="sg1">Record 2</record>
<record Group="g2" SubGroup="sg1">Record 4</record>
</subgroup>
<subgroup name="sg2">
<record Group="g2" SubGroup="sg2">Record 5</record>
</subgroup>
</group>
</data>
The problem is the for-each loop in the tempalte named "Group". It seems, that the key()-function is not working on the nodes contained in $Records but on the entire input XML file.
I get identical results with xsltproc and with saxon, so I do not think, it is a bug in my xslt processor. It seems, that I did not completely understand, how key() works.
If I add an additional selector [#Group = $Group] to key()'s output, I get the expected result.
Can somebody explain what is going on and why the additional selector [#Group = $Group] is needed.
Mario

When you want to do sub-grouping, you need to use a concatenated key of both the main group and the sub-group
<xsl:key name="SubGroup" match="record" use="concat(#Group,'|', #SubGroup)" />
Then, just use it in the same way as before, with the concatenation
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',concat(#Group,'|', #SubGroup))[1])]">
Try this XSLT (which I have also simplified to utilise the key when calling your named templates with the records)
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" version="1.0" indent="yes" encoding="UTF-8"/>
<xsl:key name="Group" match="record" use="#Group" />
<xsl:key name="SubGroup" match="record" use="concat(#Group,'|', #SubGroup)" />
<xsl:template match="/data">
<data>
<xsl:for-each select="record[generate-id(.)=generate-id(key('Group',#Group)[1])]">
<xsl:sort select="#Group"/>
<xsl:call-template name="Group">
<xsl:with-param name="Records" select="key('Group',#Group)"/>
</xsl:call-template>
</xsl:for-each>
</data>
</xsl:template>
<xsl:template name="Group">
<xsl:param name="Records"/>
<group name="{#Group}">
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',concat(#Group,'|', #SubGroup))[1])]">
<xsl:sort select="#SubGroup"/>
<xsl:call-template name="SubGroup">
<xsl:with-param name="Records" select="key('SubGroup',concat(#Group,'|', #SubGroup))"/>
</xsl:call-template>
</xsl:for-each>
</group>
</xsl:template>
<xsl:template name="SubGroup">
<xsl:param name="Records"/>
<subgroup name="{#SubGroup}">
<xsl:for-each select="$Records">
<xsl:copy-of select="."/>
</xsl:for-each>
</subgroup>
</xsl:template>
</xsl:stylesheet>

Related

Using correct regex in xslt 2.0

I've been stuck using the correct regex. I need to tokenize each data per keys along with there values. In my example below,
Sample File:
<Rec>
<Data>/CHG1/EUR1000,00/EXCH/0,10/CPRP/Payment Code 1</Data>
<Data>/CHG1/EUR1000,00/EXCH/0,10/CPRP/Payment Code 1</Data>
<Data>/CHG3/EUR3000,00/PURP//CD/Payment Code 3</Data>
<Data>/CHG5/EUR5000,00/PURP//PRTRY/Payment Code 5</Data>
<Data>/ORIG//CSID/EUR7000,00/BENM//ID/Payment Code 7</Data>
</Rec>
The keys with '//' in the middle is considered as 1 key. I need to generate the output like this:
<Data>
<Group>
<Token>/CHG1/EUR1000,00</Token>
<Token>/EXCH/0,10</Token>
<Token>/CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token>/CHG3/EUR3000,00</Token>
<Token>/PURP//CD/Payment Code 3</Token>
</Group>
<Group>
<Token>/CHG5/EUR5000,00</Token>
<Token>/PURP//PRTRY/Payment Code 5</Token>
</Group>
<Group>
<Token>/ORIG//CSID/EUR7000,00</Token>
<Token>/BENM//ID/Payment Code 7</Token>
</Group>
</Data>
But, my generated output is like this:
<Data>
<Group>
<Token>/CHG1/</Token>
<Token>/EXCH/</Token>
<Token>/CPRP/</Token>
</Group>
<Group>
<Token>/CHG3/</Token>
<Token>/PURP//CD/</Token>
</Group>
<Group>
<Token>/CHG5/</Token>
<Token>/PURP//PRTRY/</Token>
</Group>
<Group>
<Token>/ORIG//CSID/</Token>
<Token>/BENM//ID/</Token>
</Group>
</Data>
Here is my XSLT that I've been using:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()[boolean(normalize-space())]|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Data">
<xsl:for-each select=".">
<Group>
<xsl:analyze-string select="." regex="\/[A-Z]+[0-9]?\/(\/(CD|PRTRY|MARF|ID|CSID|NAME|RID)\/)?">
<xsl:matching-substring>
<xsl:variable name="val" select="."/>
<Token>
<xsl:value-of select="$val"/>
</Token>
</xsl:matching-substring>
</xsl:analyze-string>
</Group>
</xsl:for-each>
</xsl:template>
There is something missing in my regex expression. Can anyone help me figure out? Thank you for your feedback.
I can't make heads or tails of your expected output. I suspect the correct output should be actually something like:
<?xml version="1.0" encoding="UTF-8"?>
<Rec>
<Group>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token>CHG3/EUR3000,00</Token>
<Token>PURP/</Token>
<Token>CD/Payment Code 3</Token>
</Group>
<Group>
<Token>CHG5/EUR5000,00</Token>
<Token>PURP/</Token>
<Token>PRTRY/Payment Code 5</Token>
</Group>
<Group>
<Token>ORIG/</Token>
<Token>CSID/EUR7000,00</Token>
<Token>BENM/</Token>
<Token>ID/Payment Code 7</Token>
</Group>
</Rec>
If so, I would suggest you change your approach and try:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Data">
<xsl:variable name="t" select="tokenize(., '/')" />
<Group>
<xsl:for-each select="$t[position() mod 2 = 0]">
<xsl:variable name="i" select="index-of($t, .)"/>
<Token>
<xsl:value-of select="." />
<xsl:text>/</xsl:text>
<xsl:value-of select="$t[$i + 1]" />
</Token>
</xsl:for-each>
</Group>
</xsl:template>
</xsl:stylesheet>
Demo: http://xsltransform.net/93dEHGr
Or even simpler:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Data">
<Group>
<xsl:for-each-group select="tokenize(., '/')" group-by="position() idiv 2">
<Token>
<xsl:value-of select="current-group()" separator="/"/>
</Token>
</xsl:for-each-group>
</Group>
</xsl:template>
</xsl:stylesheet>
Demo: http://xsltransform.net/93dEHGr/2
Added:
Actually, the keys with 2 '//' in the middle is considered as 1 key.
Well, then let's take the // strings out of the game before doing the tokenizing, and restore them at the end:
<xsl:template match="Data">
<Group>
<xsl:for-each-group select="tokenize(replace(., '//', '§§§'), '/')" group-by="position() idiv 2">
<Token>
<xsl:value-of select="replace(string-join(current-group(), '/'), '§§§', '//')" />
</Token>
</xsl:for-each-group>
</Group>
</xsl:template>
Result
<?xml version="1.0" encoding="UTF-8"?>
<Rec>
<Group>
<Token/>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token/>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token/>
<Token>CHG3/EUR3000,00</Token>
<Token>PURP//CD/Payment Code 3</Token>
</Group>
<Group>
<Token/>
<Token>CHG5/EUR5000,00</Token>
<Token>PURP//PRTRY/Payment Code 5</Token>
</Group>
<Group>
<Token/>
<Token>ORIG//CSID/EUR7000,00</Token>
<Token>BENM//ID/Payment Code 7</Token>
</Group>
</Rec>
http://xsltransform.net/93dEHGr/4
You can try this with tokenize in xslt 2.0
<xsl:template match="Rec">
<Data>
<xsl:apply-templates/>
</Data>
</xsl:template>
<xsl:template match="Data">
<Group>
<xsl:for-each select="tokenize(., '0/')">
<Token>
<xsl:if test="position() ne 1">
<xsl:text>/</xsl:text>
</xsl:if>
<xsl:value-of select="."/>
<xsl:if test="position() ne last()">
<xsl:text>0</xsl:text>
</xsl:if>
</Token>
</xsl:for-each>
</Group>
</xsl:template>

XSLT 1.0 transformation

I'm trying to transform a XML file with XSLT 1.0 but I'm having troubles with this.
Input:
<task_order>
<Q>
<record id="1">
<column name="task_externalId">SPLIT4_0</column>
</record>
<record id="2">
<column name="task_externalId">SPLIT4_1</column>
</record>
</Q>
<task>
<id>SPLIT4</id>
<name>test</name>
</task>
</task_order>
Wanted result:
For each task_order element: When there is more than 1 record-element (SPLIT4 and SPLIT4_1) I need to duplicate the task element and change the orginal task-id with the id from record elements.
<task_order>
<Q>
<record id="1">
<column name="task_externalId">SPLIT4_0</column>
</record>
<record id="2">
<column name="task_externalId">SPLIT4_1</column>
</record>
</Q>
<task>
<id>SPLIT4_0</id>
<name>test</name>
</task>
<task>
<id>SPLIT4_1</id>
<name>test</name>
</task>
</task_order>
Any suggestions?
Thank you
First start off with the Identity Template which will handle copying across all existing nodes
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
Next, to make looking up the columns slightly easier, consider using an xsl:key
<xsl:key name="column" match="column" use="substring-before(., '_')" />
Then, you have a template matching task where you can look up all matching column elements using the key, and create a new task element for each
<xsl:template match="task">
<xsl:variable name="task" select="." />
<xsl:for-each select="key('column', id)">
<!-- Create new task -->
</xsl:for-each>
</xsl:template>
Try this XSTL
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="column" match="column" use="substring-before(., '_')" />
<xsl:template match="task">
<xsl:variable name="task" select="." />
<xsl:for-each select="key('column', id)">
<task>
<id><xsl:value-of select="." /></id>
<xsl:apply-templates select="$task/*[not(self::id)]" />
</task>
</xsl:for-each>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Duplicate Element x number of times with XSLT

I have a strange transformation that I'm trying to do.
XML looks like this:
<?xml version="1.0" standalone="yes"?>
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
And this is what it needs to look like:
<?xml version="1.0" standalone="yes"?>
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="2">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="3">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="4">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
Is something like this even possible with XSLT or should I rather just handle this in code?
Here's another way using XSLT 2.0...
XML Input
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
XSLT 2.0
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="RecordCount">
<xsl:variable name="record" select="../Record"/>
<xsl:copy-of select="."/>
<xsl:for-each select="1 to .">
<xsl:apply-templates select="$record" mode="replicate">
<xsl:with-param name="cnt" select="."/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="Record" mode="replicate">
<xsl:param name="cnt"/>
<Record name="{$cnt}">
<xsl:apply-templates select="#* except #name|node()"/>
</Record>
</xsl:template>
<xsl:template match="Record"/>
</xsl:stylesheet>
Output
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="2">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="3">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="4">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
Try following xlst
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/Parent">
<Parent>
<xsl:variable name="count" select="RecordCount" />
<xsl:call-template name="multiply">
<xsl:with-param name="maxCount" select="$count" />
<xsl:with-param name="nodeToCopy" select="Record" />
</xsl:call-template>
</Parent>
</xsl:template>
<xsl:template name="multiply">
<xsl:param name="maxCount" />
<xsl:param name="i" select="1" />
<xsl:param name="nodeToCopy" />
<xsl:choose>
<xsl:when test="$i <= $maxCount">
<xsl:element name="{name($nodeToCopy)}">
<xsl:attribute name="name">
<xsl:value-of select="$i" />
</xsl:attribute>
<xsl:copy-of select="$nodeToCopy/child::*" />
</xsl:element>
<xsl:call-template name="multiply">
<xsl:with-param name="maxCount" select="$maxCount" />
<xsl:with-param name="nodeToCopy" select="$nodeToCopy" />
<xsl:with-param name="i" select="$i+1" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise />
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
It is based on recursive calling of named template with increasing of "iterating" value. If would be something unclear just write a comment.

xsl:for-each loop with attribute matching with parameter

I've been trying to figure this our for a while, but no luck.
I have a for each loop specifically targeting to a node with specific attributes, but somehow the for each condition doesn't seem to work.
I have the following xml
<?xml version="1.0" encoding="UTF-8"?>
<application-template xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<config>
<env value="testing">
<subEnv value="env">
<group value="group2s">
<group value="group2as">
<prop value="group2Props">group2PropValues</prop>
</group>
</group>
</subEnv>
<subEnv value="misc1">
<group value="group2s">
<group value="group2as">
<prop value="group2Props">group2sPropValues</prop>
</group>
</group>
<group value="group2s1">
<group value="group2as1">
<prop value="group2Props">group2s1PropValues</prop>
</group>
</group>
</subEnv>
</env>
<env value="testingA">
<subEnv value="env">
<group value="test2">
<group value="test2a">
<group value="test2ab">
<prop value="group2Props">testingAGroup2PropValues</prop>
</group>
</group>
</group>
</subEnv>
</env>
</config>
</application-template>
with the following xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:param name="inputEnv"/>
<xsl:variable name="env" select="/application-template/config/env[#value=$inputEnv]"/>
<xsl:strip-space elements="*"/>
<xsl:output method="text" indent="no"/>
<xsl:template match="/">
<xsl:call-template name="ou">
<xsl:with-param name="subEnvPos" select="$env/subEnv"/>
<xsl:with-param name="subEnvValue" select="$env/subEnv/#value"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="ou">
<xsl:param name="subEnvPos"/>
<xsl:param name="subEnvValue"/>
<xsl:variable name="test" select="/application-template/config/env[#value=$inputEnv]/subEnv[#value=$subEnvValue]"/>
<xsl:for-each select="$test/group">
testing1:
<xsl:value-of select="$test"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The param inputEnv is "testing" and the param $subEnvValue is "env". The output I got is
testing1:group2PropValues
testing1:group2PropValues
testing1:group2PropValues
testing1:group2PropValues
But I want to just loop once since the the condition is to match the subenv node = $subEnvValue (which is "env"). The output I'm hoping for is
testing1:group2PropValues
I think that instead of your current code:
<xsl:template match="/">
<xsl:call-template name="ou">
<xsl:with-param name="subEnvPos"
select="$env/subEnv"/>
<xsl:with-param name="subEnvValue"
select="$env/subEnv/#value"/>
</xsl:call-template>
</xsl:template>
you want something like this:
<xsl:template match="/">
<xsl:call-template name="ou">
<xsl:with-param name="subEnvPos"
select="$env/subEnv[1]"/>
<xsl:with-param name="subEnvValue"
select="$env/subEnv[1]/#value"/>
</xsl:call-template>
</xsl:template>
With this modification, the complete XSLT code now is:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:param name="inputEnv" select="'testing'"/>
<xsl:variable name="env" select=
"/application-template/config/env[#value=$inputEnv]"/>
<xsl:strip-space elements="*"/>
<xsl:output method="text" indent="no"/>
<xsl:template match="/">
<xsl:call-template name="ou">
<xsl:with-param name="subEnvPos"
select="$env/subEnv[1]"/>
<xsl:with-param name="subEnvValue"
select="$env/subEnv[1]/#value"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="ou">
<xsl:param name="subEnvPos"/>
<xsl:param name="subEnvValue"/>
<xsl:variable name="test" select=
"/application-template/config/env
[#value=$inputEnv]/subEnv[#value=$subEnvValue]"/>
<xsl:for-each select="$test/group">
testing1:
<xsl:value-of select="$test"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
and when applied to the provided XML document:
<application-template xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<config>
<env value="testing">
<subEnv value="env">
<group value="group2s">
<group value="group2as">
<group value="group2bs">
<prop value="group2Props">group2PropValues</prop>
</group>
</group>
</group>
</subEnv>
<subEnv value="misc1">
<group value="group2s">
<group value="group2as">
<group value="group2bs">
<prop value="group2Props">group2sPropValues</prop>
</group>
</group>
</group>
<group value="group2s1">
<group value="group2as1">
<group value="group2bs1">
<prop value="group2Props">group2s1PropValues</prop>
</group>
</group>
</group>
</subEnv>
<subEnv value="misc2">
<group value="group2sMisc2">
<group value="group2asMisc2">
<group value="group2bsMisc2">
<prop value="group2Props">group2PropValuesMisc2</prop>
</group>
</group>
</group>
</subEnv>
</env>
</config>
</application-template>
the wanted result is produced:
testing1:
group2PropValues
Update:
After the OP explained in a comment what he wants, here is a short and simple solution to the new problem:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:param name="vinputEnv" select="'testing'"/>
<xsl:template match="/*/*/env">
<xsl:if test="#value = $vinputEnv">
<xsl:for-each select="descendant::*[not(*)]">
<xsl:value-of select="concat('testing1: ', ., '
')"/>
</xsl:for-each>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied to the provided XML document (above), the wanted, correct result is produced:
testing1: group2PropValues
testing1: group2sPropValues
testing1: group2s1PropValues
testing1: group2PropValuesMisc2

How to eliminate duplicate nodes bases on values of multiple attributes?

How can I eliminate duplicate nodes based on values of multiple (more than 1) attributes? Also the attribute names are passed as parameters to the stylesheet. Now I am aware of the Muenchian method of grouping that uses a <xsl:key> element. But I came to know that XSLT 1.0 does not allow paramters/variables in <xsl:key>.
Is there another method(s) to achieve duplicate nodes removal? It is fine if it not as efficient as the Munechian method.
Update from previus question:
XML:
<data id = "root">
<record id="1" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="2" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="3" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="4" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="5" operator1='xxx' operator2='lkj' operator3='tyu'/>
<record id="6" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="7" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="8" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="9" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="10" operator1='rrr' operator2='yyy' operator3='zzz'/>
</data>
Other approach for a single transformation in two steps:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl">
<xsl:key name="kItemByLocal" match="record[#local-key]" use="#local-key"/>
<xsl:param name="pAttNames" select="'operator1 operator2 operator3'"/>
<xsl:template match="/">
<xsl:variable name="vFirstRTF">
<xsl:apply-templates/>
</xsl:variable>
<xsl:apply-templates select="msxsl:node-set($vFirstRTF)/node()"/>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[not(#local-key)]">
<xsl:copy>
<xsl:attribute name="local-key">
<xsl:call-template name="local-key"/>
</xsl:attribute>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[#local-key]
[count(.|key('kItemByLocal',#local-key)[1])
!= 1]|#local-key"/>
<xsl:template name="local-key">
<xsl:param name="pAttributes" select="concat($pAttNames,' ')"/>
<xsl:if test="normalize-space($pAttributes)">
<xsl:variable name="vName"
select="substring-before($pAttributes,' ')"/>
<xsl:variable name="vAttribute" select="#*[name()=$vName]"/>
<xsl:value-of select="concat($vName,'+',$vAttribute,'+')"/>
<xsl:call-template name="local-key">
<xsl:with-param name="pAttributes"
select="substring-after($pAttributes,' ')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Output:
<data id="root">
<record id="1" operator1="xxx" operator2="yyy" operator3="zzz"></record>
<record id="2" operator1="abc" operator2="yyy" operator3="zzz"></record>
<record id="5" operator1="xxx" operator2="lkj" operator3="tyu"></record>
<record id="10" operator1="rrr" operator2="yyy" operator3="zzz"></record>
</data>
Edit: Also without named template for #local-key generation
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl">
<xsl:key name="kItemByLocal" match="record[#local-key]" use="#local-key"/>
<xsl:param name="pAttNames" select="'operator1 operator2 operator3'"/>
<xsl:template match="/">
<xsl:variable name="vFirstRTF">
<xsl:apply-templates/>
</xsl:variable>
<xsl:apply-templates select="msxsl:node-set($vFirstRTF)/node()"/>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[not(#local-key)]">
<xsl:variable name="vAttNames"
select="concat(' ',$pAttNames,' ')"/>
<xsl:copy>
<xsl:attribute name="local-key">
<xsl:for-each select="#*[contains(
$vAttNames,
concat(' ',name(),' ')
)]">
<xsl:sort select="substring-before(
$vAttNames,
concat(' ',name(),' ')
)"/>
<xsl:value-of select="concat(name(),'++',.,'++')"/>
</xsl:for-each>
</xsl:attribute>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[#local-key]
[count(.|key('kItemByLocal',#local-key)[1])
!= 1]|#local-key"/>
</xsl:stylesheet>
Note: If you are positive sure that attributes order is the same for all elements, then you could remove the sorting.
Use this transformation (simple and no need to generate a new stylesheet):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="pAttribs">
<name>operator1</name>
<name>operator2</name>
<name>operator3</name>
</xsl:param>
<xsl:variable name="vAttribs" select=
"document('')/*/xsl:param[#name='pAttribs']"/>
<xsl:key name="kRecByAtts" match="record"
use="#___g_key"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:variable name="vrtdPass1">
<xsl:apply-templates/>
</xsl:variable>
<xsl:variable name="vPass1" select=
"ext:node-set($vrtdPass1)/*"/>
<xsl:apply-templates select="$vPass1"/>
</xsl:template>
<xsl:template match="record[not(#___g_key)]">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:attribute name="___g_key">
<xsl:for-each select="#*[name()=$vAttribs/name]">
<xsl:sort select="name()"/>
<xsl:value-of select=
"concat('___Attrib___',name(),'___Value___',.,'+++')"/>
</xsl:for-each>
</xsl:attribute>
</xsl:copy>
</xsl:template>
<xsl:template match=
"record[#___g_key]
[not(generate-id()
=
generate-id(key('kRecByAtts', #___g_key)[1])
)
]
"/>
<xsl:template match="#___g_key"/>
</xsl:stylesheet>
When applied to the XML document of your previous question:
<data id = "root">
<record id="1" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="2" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="3" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="4" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="5" operator1='xxx' operator2='lkj' operator3='tyu'/>
<record id="6" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="7" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="8" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="9" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="10" operator1='rrr' operator2='yyy' operator3='zzz'/>
</data>
The wanted, correct result is produced:
<data id="root">
<record id="1" operator1="xxx" operator2="yyy" operator3="zzz"/>
<record id="2" operator1="abc" operator2="yyy" operator3="zzz"/>
<record id="5" operator1="xxx" operator2="lkj" operator3="tyu"/>
<record id="10" operator1="rrr" operator2="yyy" operator3="zzz"/>
</data>
If you want to pass in the attribute names as a parameter then one approach could be a two step transformation where the first step takes any XML input and simply the attribute names and the element names as parameters to generate a second stylesheet that then eliminates the duplicates.
Here is an example first stylesheet:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
xmlns:axsl="http://www.w3.org/1999/XSL/TransformAlias"
exclude-result-prefixes="axsl exsl"
version="1.0">
<xsl:param name="parent-name" select="'items'"/>
<xsl:param name="element-name" select="'item'"/>
<xsl:param name="att-names" select="'att1,att2'"/>
<xsl:param name="sep" select="'|'"/>
<xsl:namespace-alias stylesheet-prefix="axsl" result-prefix="xsl"/>
<xsl:output method="xml" indent="yes"/>
<xsl:variable name="key-value">
<xsl:text>concat(</xsl:text>
<xsl:call-template name="define-values">
<xsl:with-param name="att-names" select="$att-names"/>
</xsl:call-template>
<xsl:text>)</xsl:text>
</xsl:variable>
<xsl:template name="define-values">
<xsl:param name="att-names"/>
<xsl:choose>
<xsl:when test="contains($att-names, ',')">
<xsl:value-of select="concat('#', substring-before($att-names, ','), ',"', $sep, '",')"/>
<xsl:call-template name="define-values">
<xsl:with-param name="att-names" select="substring-after($att-names, ',')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('#', $att-names)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="/">
<axsl:stylesheet version="1.0">
<axsl:output indent="yes"/>
<axsl:key name="k1" match="{$parent-name}/{$element-name}" use="{$key-value}"/>
<axsl:template match="#* | node()">
<axsl:copy>
<axsl:apply-templates select="#* | node()"/>
</axsl:copy>
</axsl:template>
<axsl:template match="{$parent-name}">
<axsl:copy>
<axsl:apply-templates select="#*"/>
<axsl:apply-templates select="{$element-name}[generate-id() = generate-id(key('k1', {$key-value})[1])]"/>
</axsl:copy>
</axsl:template>
</axsl:stylesheet>
</xsl:template>
</xsl:stylesheet>
It takes four parameters:
parent-name: the name of the element containing those elements of which you want to eliminate duplicates
element-name: the name of those elements of which you want to eliminate duplicates
att-names: a comma separated list of attribute names
sep: a separator character that should not occur in attribute values in the input XML
The stylesheet then generates a second stylesheet that applies Muenchian grouping to eliminate duplicates. For instance with the default parameters given in the stylesheet Saxon 6.5.5 generates the following stylesheet:
<axsl:stylesheet xmlns:axsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<axsl:output indent="yes"/>
<axsl:key name="k1" match="items/item" use="concat(#att1,"|",#att2)"/>
<axsl:template match="#* | node()">
<axsl:copy>
<axsl:apply-templates select="#* | node()"/>
</axsl:copy>
</axsl:template>
<axsl:template match="items">
<axsl:copy>
<axsl:apply-templates select="#*"/>
<axsl:apply-templates select="item[generate-id() = generate-id(key('k1', concat(#att1,"|",#att2))[1])]"/>
</axsl:copy>
</axsl:template>
</axsl:stylesheet>
This can the be applied to an XML document like
<items>
<item att1="a" att2="1" att3="A"/>
<item att1="b" att2="1" att3="A"/>
<item att1="a" att2="1" att3="B"/>
<item att1="c" att2="2" att3="A"/>
<item att1="d" att2="3" att3="C"/>
</items>
and the output is
<items>
<item att1="a" att2="1" att3="A"/>
<item att1="b" att2="1" att3="A"/>
<item att1="c" att2="2" att3="A"/>
<item att1="d" att2="3" att3="C"/>
</items>