Walk/loop through an XSL key: how?

23,620

Solution 1

You can't. That's not what keys are for.

You can loop through every element in a key using a single call to key() if and only if the key of each element is the same.

If you need to loop over everything the key is defined over, you can use the expression in the match="..." attribute of your <key> element.

So if you had a file like this:

<root>
  <element name="Bill"/>
  <element name="Francis"/>
  <element name="Louis"/>
  <element name="Zoey"/>
</root>

And a key defined like this:

<xsl:key name="survivors" match="element" use="@name"/>

You can loop through what the key uses by using the contents of its match attribute:

<xsl:for-each select="element">
  <!-- stuff -->
</xsl:for-each>

Alternatively, if each element had 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 could define your key like this:

<xsl:key name="survivors" match="element" use="@class"/>

And iterate over all elements like this:

<xsl:for-each select="key('survivors', 'survivor')">
  <!-- stuff -->
</xsl:for-each>

Because each element shares the value "survivor" for the class attribute.

In your case, your key is

<xsl:key name="kElement" match="Element/Element[@idref]" use="@idref" />

So you can loop through everything it has like this:

<xsl:for-each select="Element/Element[@idref]">
  <!-- stuff -->
</xsl:for-each>

Solution 2

You CAN create a key to use for looping - if you simply specify a constant in the use attribute of the key element:

<xsl:key name="survivors" match="element" use="'all'"/>

Then you can loop over all elements in the following way:

<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 'all' reads well.

However, you cannot use this key to lookup information about the individual entries (because they all have the same key).

In other words there are two types of possible keys:

  1. "lookup keys" = standard keys with varying indexes in the use attribute
  2. "looping keys" = keys with a constant in the use attribute

I do not know how efficient this method is to execute, it does however make the maintenance of the XSL more efficient by avoiding repetition of the same (potentially very complex) XPath expression throughout the XSL code.

Solution 3

Rather than think of the XSL keys in programming language terms, think of them as record sets of SQL. That will give a better understanding. For a given key index created as

<xsl:key name="paths" match="path" use="keygenerator()">

it can be "iterated"/"walk-through" as below

<xsl:for-each select="//path[generate-id()=generate-id(key('paths',keygenerator())[1])]">

To understand this magic number [1], let s go through the below example :

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 id of the Person nodes and the cost nodes in the form of idpxxxxxxx as the result below shows.

  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 us create a key on the cost records using a combination of name and itemID values.

 <xsl:key name="keyByNameItem" match="cost" use="concat(../name, '+', @itemID)"/>

Manually looking at the XML, the number of unique keys for the above would be three : Johny+1, Johny+2 and Johny+3.

Now lets test out this key by 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 is 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 in trying to understand the importance of [1],[2], [3],[4]. In our case, the keygenerator is concat(../name, '+', @itemID).

For a given key, [1] refers to the first occurence of a node that satisfies the keygenerator. Similarly [2] refers to the second occurence of a node that satisfies the keygenerator. Thus [2], [3],[4], etc. are all nodes that satisfy the same key, and thus can be considered duplicates for the given key. The number of duplicates depends on the input XML. Thus:

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)

Thus we see that ALL 8 cost nodes of the XML can be accessed through the key.

Here is a image that combines the transformation results to help better understand.

enter image description here

The red squares indicate the matching nodes for Johny+1. The green squares indicate the matching nodes for Johny+3. Match the idpxxxxxxx values in <costkeygroup> to the values in <costrecords>. The <costrecords> help map the idpxxxxxxx values to the source XML.

The takeaway is that,

an XSL key does not filter or eliminate nodes. All nodes including duplicates can be accessed through the key. Thus when we say "walk through" of the key, there is no concept of a resultant subset of nodes from the original set of nodes made available to the key for processing.

To "walk through" only unique nodes of the key in the above example, use

 <xsl:for-each select="*/*/workTime[generate-id()=generate-id(key('keyByNameItem', concat(../name, '+', @itemID) )[1] ) ] ">

[1] signifies that the first record for a given key value is denoted as the unique record. [1] is almost always used because there will exist at least one node that satisfies a given key value. If we are sure that there will be a minimum of 2 records to satisfy each key value in the key, we can go ahead and use [2] to identify the second record in the record set as the unique record.

P.S The words nodes / records / elements are used interchangeably.

Share:
23,620
Kris Van den Bergh
Author by

Kris Van den Bergh

Updated on February 12, 2020

Comments

  • Kris Van den Bergh
    Kris Van den Bergh about 4 years

    Is there a way to walk-through a key and output all the values it contains?

       <xsl:key name="kElement" match="Element/Element[@idref]" use="@idref" />
    

    I though of it this way:

    <xsl:for-each select="key('kElement', '.')">
      <li><xsl:value-of select="." /></li>
    </xsl:for-each>
    

    However, this does not work. I simply want to list all the values in a key for testing purposes.

    The question is simply: how can this be done?