Walking / looping through an XSL key: how?
Is there a way to go through the key and output all the values it contains?
<xsl:key name="kElement" match="Element/Element[@idref]" use="@idref" />
I thought so:
<xsl:for-each select="key('kElement', '.')">
<li><xsl:value-of select="." /></li>
</xsl:for-each>
However, this doesn't work. I just want to list all the values in a key for testing purposes.
The question is simple: how can this be done?
a source to share
You can not. You don't need keys for this.
You can scroll through each item in a key with a single click on key()
if and only if the key of each item is the same.
If you need to iterate over everything defined for a key, you can use an expression in an attribute of match="..."
your element <key>
.
So, if you have a file like this:
<root>
<element name="Bill"/>
<element name="Francis"/>
<element name="Louis"/>
<element name="Zoey"/>
</root>
And the key is defined like this:
<xsl:key name="survivors" match="element" use="@name"/>
You can skip what the key is using by using its attribute match
:
<xsl:for-each select="element">
<!-- stuff -->
</xsl:for-each>
Alternatively, if each item has something in common:
<root>
<element name="Bill" class="survivor"/>
<element name="Francis" class="survivor"/>
<element name="Louis" class="survivor"/>
<element name="Zoey" class="survivor"/>
</root>
Then you can define your key like this:
<xsl:key name="survivors" match="element" use="@class"/>
And iterate over all the elements as follows:
<xsl:for-each select="key('survivors', 'survivor')">
<!-- stuff -->
</xsl:for-each>
As each element shares the "survivor" value for the attribute class
.
In your case, your key
<xsl:key name="kElement" match="Element/Element[@idref]" use="@idref" />
So, you can view whatever you want:
<xsl:for-each select="Element/Element[@idref]">
<!-- stuff -->
</xsl:for-each>
a source to share
You can create a key to use for looping - if you just specify a constant in the use attribute of the key element:
<xsl:key name="survivors" match="element" use="'all'"/>
Then you can iterate over all the elements like this:
<xsl:for-each select="key('survivors','all')">
...
</xsl:for-each>
Or count them:
<xsl:value-of select="count(key('survivors','all'))"/>
Note that the constant can be any string, or even a number - but "everything" reads well.
However, you cannot use this key to find information about individual records (since they all have the same key).
In other words, there are two types of possible keys:
- "search keys" = standard keys with different indexes in the use attribute
- "looping keys" = keys with a constant in the use attribute
I don't know how efficient this technique is to execute, however it makes serving the XSL more efficient by avoiding repeating the same (potentially very complex) XPath expression throughout the XSL code.
a source to share
Instead of thinking of XSL keys in terms of a programming language, think of them as SQL recordsets. This will give a better understanding. For a given key index, created as
<xsl:key name="paths" match="path" use="keygenerator()">
it can be "iterated" / "walked through" as shown below
<xsl:for-each select="//path[generate-id()=generate-id(key('paths',keygenerator())[1])]">
To understand this magic number [1]
, let s consider the example below:
Consider this XML snippet
<root>
<Person>
<name>Johny</name>
<date>Jan10</date>
<cost itemID="1">34</cost>
<cost itemID="1">35</cost>
<cost itemID="2">12</cost>
<cost itemID="3">09</cost>
</Person>
<Person>
<name>Johny</name>
<date>Jan09</date>
<cost itemID="1">21</cost>
<cost itemID="1">41</cost>
<cost itemID="2">11</cost>
<cost itemID="2">14</cost>
</Person>
</root>
transformed using this XSL.
<xsl:for-each select="*/Person">
<personrecords>
<xsl:value-of select="generate-id(.)" />--
<xsl:value-of select="name"/>--
<xsl:value-of select="date"/>--
</personrecords>
</xsl:for-each>
<xsl:for-each select="*/*/cost">
<costrecords>
<xsl:value-of select="generate-id(.)" />--
<xsl:value-of select="../name"/>--
<xsl:value-of select="../date"/>--
<xsl:value-of select="@itemID"/>--
<xsl:value-of select="text()"/>
</costrecords>
</xsl:for-each>
The above XSL transformation lists the unique identifier of nodes Person
and nodes cost
in the form idpxxxxxxx
as shown below.
1. <personrecords>idp2661952--Johny--Jan10-- </personrecords>
2. <personrecords>idp4012736--Johny--Jan09-- </personrecords>
3. <costrecords>idp2805696--Johny-- Jan10-- 1-- 34</costrecords>
4. <costrecords>idp4013568--Johny-- Jan10-- 1-- 35</costrecords>
5. <costrecords>idp2808192--Johny-- Jan10-- 2-- 12</costrecords>
6. <costrecords>idp2808640--Johny-- Jan10-- 3-- 09</costrecords>
7. <costrecords>idp2609728--Johny-- Jan09-- 1-- 21</costrecords>
8. <costrecords>idp4011648--Johny-- Jan09-- 1-- 41</costrecords>
9. <costrecords>idp2612224--Johny-- Jan09-- 2-- 11</costrecords>
10.<costrecords>idp2610432--Johny-- Jan09-- 2-- 14</costrecords>
Let's create a key in records cost
using a combination of values name
and itemID
.
<xsl:key name="keyByNameItem" match="cost" use="concat(../name, '+', @itemID)"/>
In a manual XML search, the number of unique keys for the above would be three: Johny + 1 , Johny + 2, and Johny + 3 .
Now let's test this key using the snippet below.
<xsl:for-each select="*/*/cost">
<costkeygroup>
<xsl:value-of select="generate-id(.)" />--
(1)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', @itemID) )[1] ) " />--
(2)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', @itemID) )[2] ) " />--
(3)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', @itemID) )[3] ) " />--
(4)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', @itemID) )[4] ) " />
</costkeygroup>
</xsl:for-each>
And here's the result:
1. <costkeygroup>idp2805696-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
2. <costkeygroup>idp4013568-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
3. <costkeygroup>idp2808192-- (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)</costkeygroup>
4. <costkeygroup>idp2808640-- (1)idp2808640-- (2)-- (3)-- (4)</costkeygroup>
5. <costkeygroup>idp2609728-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
6. <costkeygroup>idp4011648-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
7. <costkeygroup>idp2612224-- (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)</costkeygroup>
8. <costkeygroup>idp2610432-- (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)</costkeygroup>
Our interest is to try to understand the importance of [1]
, [2]
, [3]
, [4]
. In our case, the key generator concat(../name, '+', @itemID)
.
For a given key, [1]
refers to the first node event that satisfies the key generator. Similarly [2]
applies to the second occurrence of the node that satisfies the key generator. Thus [2]
, [3]
, [4]
etc. - all nodes that match the same key and thus can be considered duplicates for a given key. The number of duplicates depends on the input XML. In this way:
Key Johny + 1 satisfies 4 nodes (1) idp2805696-- (2) idp4013568-- (3) idp2609728-- (4) idp4011648
Key Johny + 2 satisfies 3 nodes (1) idp2808192-- (2) idp2612224-- (3 ) idp2610432-- (4)
Key Johny + 3 satisfies 1 node (1) idp2808640-- (2) - (3) - (4)
So we can see that ALL 8cost
XML nodes can be accessed via a key.
Here is an image that combines the conversion results for a better understanding.

The red squares indicate the matching nodes for Johny + 1 . Green squares indicate matching nodes for Johny + 3 . Match the values idpxxxxxxx
in <costkeygroup>
with the values in <costrecords>
. <costrecords>
help map values idpxxxxxxx
to the original XML.
The conclusion is that
The XSL key does not filter or remove nodes. All nodes, including duplicates, can be accessed through a key. Thus, when we say "traverse" the key, there is no concept of the resulting subset of nodes from the original set of nodes available for the key to process.
To "skip" only unique key nodes in the above example, use
<xsl:for-each select="*/*/workTime[generate-id()=generate-id(key('keyByNameItem', concat(../name, '+', @itemID) )[1] ) ] ">
[1]
means the first record for a given key value is designated as a unique record. [1]
is almost always used because there will be at least one node that matches the given key value. If we are confident that there will be at least 2 records for each key value in the key, we can proceed and use [2]
to identify the second record in the record set as a unique record.
PS The words nodes / records / elements are used interchangeably.
a source to share
There is no way to go through the keys, although we can output all the values it contains. In XSLT2 it is much simpler than in XSLT1 (for example using fn:generate-id
as per the previous answer).
Using fn:distinct-values
<xsl:variable name="e" select="."/>
<xsl:for-each select="distinct-values(Element/Element[@idref]/@idref)">
<li key="{.}"><xsl:value-of select="key('kElement', ., $e )" /></li>
</xsl:for-each>
Using xsl:for-each-group
<xsl:for-each-group select="Element/Element[@idref]" group-by="@idref">
<li key="{current-grouping-key()}"><xsl:value-of select="current-group()" /></li>
</xsl:for-each-group>
a source to share