Walk/loop through an XSL key: how? - xslt

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?

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>

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:
"lookup keys" = standard keys with varying indexes in the use attribute
"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.

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.
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.

There is no way to walk-through the keys, although we can output all the values it contains. In XSLT2 it is quite easier than in XSLT1 (e.g., using fn:generate-id according to 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>

Related

Comparing attribute values in XPath

I want to get the value of a specific node, specified by its id attribute. But the behaviour of my XSL parser, Saxon, is not how I expected it to work.
This is my XSL code:
<xsl:template match="synonyme">
<xsl:element name="corr">
<xsl:value-of select="#connecteur" />
<xsl:value-of select="/liste/connecteur[#id=#connecteur]/forme[1]" />
</xsl:element>
</xsl:template>
I just matched a tag named synonyme which has a connecteur attribute. My code outputs the value of this attribute.
I also want to output the value of another node which has an id attribute matching the connecteur attribute of my currently matched synonyme tag. But no results are ever found for this query, the second value-of always has empty output.
If I write, e.g. [#id='c160'], where c160 is the exact same thing that is output by the first value-of, it works! But not when comparing to the #attribute of my matched node. How can I fix this?
The XML is basically
<liste><connecteur id="c160"><forme>foo</forme></connecteur>
<connecteur id="c161"><synonyme connecteur="c160" /></connecteur>
</liste>
and the expected output in place of the synonyme is <corr>c160 foo</corr>.
The predicate you use:
[#id=#connecteur]
is looking for an element with two attributes - id and connecteur- with equal values. To look for an element with an id attribute whose value matches the value of the current element's connecteur value, you need to use:
[#id=current()/#connecteur]
See: https://www.w3.org/TR/xslt/#function-current
A better solution would be to define a key as:
<xsl:key name="ref" match="connecteur" use="#id" />
then use:
<xsl:value-of select="key('ref', #connecteur)/forme" />
to resolve the cross-reference.
See: https://www.w3.org/TR/xslt/#key

Using same data element name within for-each

More for reference than actual need: what is the XPath syntax to allow me to reference an element in a xsl:for-each block when the same element name is used elsewhere?
Please note, unfortunately this must be a 1.0 solution
For example, I have the following simple XML, and I want to match up the items with the same id value...
<data>
<block1>
<item><id>1</id><text>Hello</text></item>
<item><id>2</id><text>World</text></item>
</block1>
<block2>
<item><id>1</id><text>123</text></item>
<item><id>2</id><text>ABC</text></item>
</block2>
</data>
If I have a for-each on the block1, how can I reference both the id within the block1 and the id within the block2?
This will work, but I think it is messy...
<xsl:for-each select="//block1/item">
<xsl:variable name="id" select="id"/>
<xsl:value-of select="text"/> - <xsl:value-of select="//block2/item[id=$id]/text"/>
</xsl:for-each>
With the result of...
Hello - 123
World - ABC
Is there a simplified way of replacing the $id in select="//block2/item[id=$id]/text" so that it is referring to the id element from the for-each?
Another way to do it which you may find clearer, and will probably be faster, is to use keys:
<xsl:key name="b2" match="block2/item" use="id"/>
then
<xsl:value-of select="key('b2', id)/text"/>
What you have is correct and common as it is. There's no need to simplify it further; it's a standard idiom recognized and used by those working with XSLT.

xslt group by in multiple nodes

A sample structure of my input xml looks as below:
<Products>
<Product>
<ID>Product1</ID>
<Extra1></Extra1>
<Extra2></Extra2>
<Img1>val1</Img1>
<Img2>val2</Img2>
<Img3>val2</Img3>
<Img4>val1</Img4>
</Product>
<Product>
<ID>Product2</ID>
<Extra1></Extra1>
<Extra2></Extra2>
<Img1>val1</Img1>
<Img2>val2</Img2>
<Img3>val2</Img3>
<Img4>val1</Img4>
</Product>
</Products>
I am parsing each product element at a time and grouping the Img tags of each product by its value. I am using <xsl:key name="keyImg" match="Product/*[contains(local-name(), 'Img')]"
use="."/>. But the same key is used over and over again. Will it be an issue if two product contains same value for some Img tag? I am not sure if there will be conflicts in such cases. Please guide. Thanks in advance.
Use a two-part key, like this:
<xsl:key
name="keyImg"
match="Product/*[starts-with(name(), 'Img')]"
use="concat(generate-id(..), '-', .)"
/>
and
<xsl:template match="Product">
<xsl:variable name="productID" select="generate-id()" />
<xsl:for-each select="*[starts-with(name(), 'Img')][
generate-id() = generate-id(key(concat($productID, '-', .)))
]">
<!-- ... --->
</xsl:for-each>
</xsl:template>
On a general note it's not ideal if nodes with the same semantic value (Img) have different names (Img1, Img2, etc). If you can do anything about it, just name them Img.
Q. But the same key is used over and over again. Will it be an issue if two product contains same value for some Img tag? I am not sure if there will be conflicts in such cases.
Whether there is a "conflict" or not depends of the wanted result. Your key holds all Img* items with the same value in a list. With this key you can generate an unique list of all Img* values.
If the key should be unique for Img* values within one Product you may add the generate-id() or the "Product/ID" to the key.
Something like this:
<xsl:key name="keyImg" match="Product/*[starts-with(local-name(), 'Img')]"
use="concat(../ID, '|', .)"/>
If the name of Img* tages is well known you may also try this:
<xsl:key name="keyImg" match="Img1 | Img2 | Img2 | Img4"
use="concat(../ID, '|', .)"/>

Change text of elements identified by dynamic XPath

I have an XML with 2 XML fragments, 1st one is a fragment where the new values must be applied (which can have pretty complex elements) like
... some static parents
<a:element1>
<a:subelement tag="someString">
<a:s1>a</a:s1>
</a:subelement>
</a:element1>
<a:element2>b</a:element2>
<a:element3>c</a:element3>
... lots of other elements like the above ones
and 2nd fragment that has XPaths generated from the first XML and a new value, like
<field>
<xpath>/Parent/element1/subelement[#tag="someString"]/s1</xpath>
<newValue>1</newValue>
</field>
<field>
<xpath>/Parent/element2</xpath>
<newValue>2</newValue>
</field>
We might not have new values to apply for all the elements in the first fragment.
I'm struggling to make an XSLT transformation that should apply the new values to the places indicated by the XPaths.
The output should be:
... some static parents
<a:element1>
<a:subelement tag="someString">
<a:s1>1</a:s1>
</a:subelement>
</a:element1>
<a:element2>2</a:element2>
... lots of other elements like the above ones
I have access to xalan:evaluate to evaluate the dynamic xpath. I'm trying different solutions, I will write them here when they will start to make sense.
Any ideas of approaches are well received. Thanks
Oki, I found out how, and I will write the answer here maybe someone sometime will need this:
<xsl:template match="/">
<!-- static parents -->
<a:Root>
<xsl:apply-templates select="/a:Root/a:Parent" />
</a:Root>
</xsl:template>
<xsl:template match="#*|*|text()">
<xsl:variable name="x" select="generate-id(../.)" />
<xsl:variable name="y" select="//field[generate-id(xalan:evaluate(xpath)) = $x]" />
<xsl:choose>
<xsl:when test="$y">
<xsl:value-of select="$y/newValue" />
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="#*|*|text()" />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
And to explain the transformation:
I'm writing down part that is static and then call apply-templates on the fragment I'm interested in, that has a liquid structure.
Then I'm using a slightly modified identity transformation that copies everything from source to target (starting from the /a:Root/a:Parent fragment), except when we position ourselves on the text I'm interested in changing.
The text() I'm interested in will have as parent (../.) the element referred by an xpath string found in the second fragment. Variable x means, in the context of the when, this element.
Variable y finds a field element that has as child an xpath element that if evaluated using xalan will refer to the same element that the x variable relates to.
Now I used generate-id() in order to compare the physical elements, otherwise it would have compared by the toString of the element (which is wrong). If variable y doesn't exist, it means that I have no xpath element for this element that could have changed, and I'm leaving it alone. If the y variable exists, I can get from it the newValue and I'm currently positioned on the element which text I want to update.

convert <xsl:variable> string value , to the value which I can select with <xsl:value-of select>

I have a variable from which I need to dynamically generate nodes
<xsl:template match="banner_discount_1 | banner_discount_2 | banner_discount_3">
<xsl:variable name="link">banner_discount_<xsl:value-of select="substring-after(name(.) ,'banner_discount_')" />_link</xsl:variable>
<xsl:value-of select="$link" />
</xsl:template>
<xsl:value-of> selects the string, but I want to be able to select the node which name matches the name of a variable.
In my case the node looks something like this:
<banner_discount_1_link />
<banner_discount_2_link />
...
Here is the xml I'm using
<banner_discount_1> 12 </banner_discount_1>
<banner_discount_2> 21 </banner_discount_2>
<banner_discount_3> 32 </banner_discount_3>
<banner_discount_1_link> link1 </banner_discount_1_link>
<banner_discount_2_link> link2 </banner_discount_2_link>
<banner_discount_3_link> link3 </banner_discount_3_link>
#MartinHonnen is on the right track, but you need to set the selection context as well.
Since you're in a template that's selecting the banner_discount_ nodes, that is your context. From your XML sample, it looks like the nodes you want to select are siblings, so this should work:
<xsl:value-of select="../*[local-name() = $link]"/>
It is preferable to target the nodes directly, but if they could be anywhere in the document, then you may resort to
<xsl:value-of select="//*[local-name() = $link]"/>
This is a last resort because it is potentially O(n) with respect to the number of nodes in the document.
Use <xsl:value-of select="*[local-name() = $link]"/>. If that does not help then consider to show a sample of the XML.