XPath relative path in expression - xslt

I am in 'group' node. From it, I want to find such 'item' node, that has 'id' attribute equals to current's 'group' node 'ref_item_id' attribute value. So in my case, by being in 'group' node B, I want 'item' node A as output. This works:
<xsl:value-of select="preceding-sibling::item[#id='1']/#description"/>
But this doesn't (gives nothing):
<xsl:value-of select="preceding-sibling::item[#id=#ref_item_id]/#description"/>
When I type:
<xsl:value-of select="#ref_item_id"/>
I have '1' as result. So this attribute is for sure accessible, but I can't find path to it from XPath expression above. I tried many '../' combinations, but couldn't get it work.
Code to test: http://www.xmlplayground.com/7l42fo
Full XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item description="A" id="1"/>
<item description="C" id="2"/>
<group description="B" ref_item_id="1"/>
</root>
Full XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no"/>
<xsl:template match="root">
<xsl:for-each select="group">
<xsl:value-of select="preceding-sibling::item[#id=#ref_item_id]/#description"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

This has to do with context. As soon as you enter a predicate, the context becomes the node currently being filtered by the predicate, and no longer the node matched by the template.
You have two options - use a variable to cache the outer scope data and reference that variable in your predicate
<xsl:variable name='ref_item_id' select='#ref_item_id' />
<xsl:value-of select="preceding-sibling::item[#id=$ref_item_id]/#description"/>
or make use of the current() function
<xsl:value-of select="preceding-sibling::item[#id=current()/#ref_item_id]/#description"/>

Your expression searches for an item whose id attribute matches its own ref_item_id. You need to capture the current ref_item_id in an xsl:variable and refer to that xsl:variable in the expression.

One more possible solution using xsl:key
<xsl:key name="kItemId" match="item" use="#id" />
<xsl:template match="root">
<xsl:for-each select="group">
<xsl:value-of select="key('kItemId', #ref_item_id)[1]/#description"/>
</xsl:for-each>
</xsl:template>

Looking at the XML, if I assume that you have <item> and <group> as siblings and in any order.
Then a sample input XML would look like the following.
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item description="A" id="1"/>
<item description="C" id="2"/>
<group description="B" ref_item_id="1"/>
<item description="D" id="1"/>
<group description="E" ref_item_id="2"/>
</root>
Now, if the goal is to extract the description of all the <item> nodes whose id is matching with corresponding <group> *nodes ref_item_id*. Then we can simply loop over only such <item> nodes and get their description.
<xsl:output method="text" indent="no"/>
<xsl:template match="root">
<xsl:for-each select="//item[(./#id=following-sibling::group/#ref_item_id) or (./#id=preceding-sibling::group/#ref_item_id)]">
<xsl:value-of select="./#description"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Since you say that nodes are having unique id and all nodes are placed before nodes.
I would recommend you to use the following XSL and loop over specific nodes instead of nodes.
<xsl:output method="text" indent="no"/>
<xsl:template match="root">
<xsl:for-each select="//item[./#id=following-sibling::group/#ref_item_id]">
<xsl:value-of select="./#description"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Related

Why variable goes wrong as "document-node()" in sub xsl:template?

Looking directly at the example:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="3.0">
<xsl:template match="/">
<xsl:variable name="v1" select="." as="document-node()"/>
<xsl:apply-templates select=".//name"/>
</xsl:template>
<xsl:template match="name">
<xsl:variable name="v2" select="." as="document-node()"/>
<xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
source xml:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item>
<name>John</name>
<age>21</age>
</item>
</root>
The question is that: variable v1 can be declarated well, but v2 has an error "Required item type of value of variable $v2 is document-node(); supplied expression (.) has item type element(Q{}name)".
I looked up the definition of document-node() in w3c xpath document, it says "document-node() matches any document node." 2.5.5.2 Matching an ItemType and an Item. I still can't understand why v2 is wrong with type document-node().
I have make a online test demo
Because it's not a document-node(), it's an element(). Use element() instead. <xsl:template match="/"> matches a document-node(), and <xsl:template match="name"> matches an element() node.

msxsl:node-set() not recognized

I am trying to pull nodes out of a node set stored in a variable using the msxsl:node-set() function and am not getting anything. My xml looks like this:
<Root>
<Items olditemnumber="100" newitemnumber="200">
<Item ItemNumber="100" ItemAliasCode="1001" ItemCode="X" />
<Item ItemNumber="100" ItemAliasCode="1002" ItemCode="X" />
<Item ItemNumber="200" ItemAliasCode="2001" ItemCode="X" />
<Item ItemNumber="200" ItemAliasCode="2003" ItemCode="X" />
<Item ItemNumber="100" ItemAliasCode="1003" ItemCode="P" />
<Item ItemNumber="100" ItemAliasCode="1004" ItemCode="P" />
<Item ItemNumber="200" ItemAliasCode="2002" ItemCode="P" />
</Items>
</Root>
In my xslt I try to populate a variable with a subset of the nodes and then call them using the msxsl:node-set() function. This doesn't return anything however.
XSLT looks like this:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:template match="//Root">
<xsl:variable name="OldItemNumber" select="/Items/#olditemnumber"/>
<xsl:variable name="NewItemNumber" select="/Items/#newitemnumber"/>
<xsl:variable name="OldItems">
<xsl:value-of select="//Item[#ItemNumber = $OldItemNumber]"/>
</xsl:variable>
<xsl:variable name="NewItems">
<xsl:value-of select="//Item[#ItemNumber = $NewItemNumber]"/>
</xsl:variable>
<xsl:for-each select="msxsl:node-set($OldItems)/Item">
...work
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The XSLT skips over the for-each loop, though I see in the watch that the the Xpath query grabs the right nodes in assigning the variables. The watch also tells me that the msxsl:node-set() function is undefined. Any help would be appreciated. What am I missing?
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:template match="//Root">
<xsl:variable name="OldItemNumber" select="/Items/#olditemnumber"/>
<xsl:variable name="NewItemNumber" select="/Items/#newitemnumber"/>
<xsl:variable name="OldItems" select="//Item[#ItemNumber = $OldItemNumber]"/>
<xsl:variable name="NewItems" select="//Item[#ItemNumber = $NewItemNumber]"/>
<xsl:for-each select="$OldItems">
...work
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
msxsl:node-set is for converting a result tree fragment (a.k.a. RTF) to a node set, which is not needed on your case.
xsl:value-of is for creating text nodes, so don't use it for selecting nodes of the input tree that you want to further query/process.

How to instruct XSLT to apply template only on children?

When applyng this XSLT:
<xsl:template match="e">
<xsl:value-of select="#name"/>
</xsl:template>
To this xml:
<root>
<e name="1"/>
<la>
<e name="bla"/>
</la>
</root>
I get both "1" and "bla".
Why is this so?
How can I make sure that the XSLT is applied only to the direct children of root?
Did you try match="root/e"? If you want to match nodes in a certain context, you need to provide the context in the rule, otherwise all nodes with the matching node name apply to the rule.
You may also use something like:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="root">
<xsl:apply-templates select="child::e"/>
</xsl:template>
<xsl:template match="e">
<xsl:value-of select="#name"/>
</xsl:template>
</xsl:stylesheet>

Trouble with xsl:for-each

I must be missing some fundamental concept of processing an XML document. Here is my source XML:
<?xml version="1.0" encoding="ISO-8859-1"?>
<Root>
<Element>visitorNameAlt</Element>
<Element>visitorScore</Element>
<Element>visitorTimeouts</Element>
<Element>Blank</Element>
<Element>homeNameAlt</Element>
<Element>homeScore</Element>
<Element>homeTimeouts</Element>
<Element>Blank</Element>
<Element>period</Element>
<Element>optionalText</Element>
<Element>flag</Element>
<Element>Blank</Element>
<Element>scoreLogo</Element>
<Element>sponsorLogo</Element>
</Root>
And my XSL stylesheet:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="/Root">
<xsl:value-of select="position()"/>
<xsl:value-of select="Element"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
All I want is to pluck the "Element" names from the source XML doc with their relative position in front.
My output is just "1" followed by the first element and nothing more.
I am new to XSLT, but have processed other documents successfully with for-each.
Thanks in advance.
Bill
You're looping over Root tags, not Element tags. Try this:
<xsl:template match="/">
<xsl:for-each select="/Root/Element">
<xsl:value-of select="position()"/>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
Note that you must change the second value-of select to "." or "text()".
XSLT is not an imperative programming language. The XSLT processor grabs each element in turn and tries to match it to your stylesheet. The idiomatic way to write this is without a for-each:
<xsl:template match="/Root">
<xsl:apply-templates select="Element"/>
</xsl:template>
<xsl:template match="Element">
<xsl:value-of select="position()"/>
<xsl:value-of select="."/>
</xsl:template>
The first template matches the root and tells the processor to apply the stylesheet to all the Element nodes inside the Root. The second template matches those nodes, and outputs the desired information.

How to restrict which nodes produce output in stylesheet

I am working on a transform. The goal is to transform nodes into key/value pairs. Found a great stylesheet recommendation on this forum but I could use some help to tweak it a bit. For any node that has no children, the node name should become the value of <name> and the value should become the value of <value>. The source document may have some hierarchical structure to it, but I want to ignore that and only return the bottom nodes, transformed of course.
Here is my source data:
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<Technical_Spec__c>
<Id>a0e30000000vFmbAAE</Id>
<F247__c>4.0</F247__c>
<F248__c xsi:nil="true"/>
<F273__c>Bronx</F273__c>
...
</Technical_Spec__c>
</objects>
Here is the stylesheet:
<?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" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="*[count(*) = 0]">
<item>
<name>
<xsl:value-of select="name(.)" />
</name>
<value>
<xsl:value-of select="." />
</value>
</item>
</xsl:template>
<xsl:template match="*[count(*) > 0]">
<items>
<xsl:apply-templates/>
</items>
</xsl:template>
</xsl:stylesheet>
DESIRED OUTPUT - The stylesheet should transform these nodes to key value pairs like this:
<items>
<item>
<name>F247__c</name>
<value>4.0</value>
</item>
<item>
<name>F248__c</name>
<value></value>
</item>
<item>
<name>F273__c</name>
<value>Bronx</value>
</item>
...
</items>
CURRENT OUTPUT - But it creates nested 'items' elements like this:
<items>
<items>
<item><name></name><value></value></item>
...
</items>
</items>
I understand (I think) that it is matching all the parent nodes including the top node 'objects' and nesting the 'matches count 0' template. So I tried altering the matches attribute to exclude 'objects' and start at 'Technical_Spec__c' like this (just the template lines):
<xsl:template match="objects/Technical_Spec__c/*">
<xsl:template match="*[count(*) = 0]">
<xsl:template match="objects/*[count(*) > 0]">
In my mind this says "First (master) template only matches nodes with parents 'objects/Tech_Spec'. Second (inner) template matches any node with no children. Third (outer) template matches nodes with parent 'objects' " - which should limit me to one .
OUTPUT AFTER ALTERING MATCH - Here is what I get:
<?xml version="1.0" encoding="UTF-8"?>
- <items xmlns=""><?xml version="1.0"?>
<item><name>Id</name><value>a0e30000000vFmbAAE</value></item>
<item><name>F247__c</name><value>4.0</value></item>
...
</items>
The extra <items> block is gone but there is an extra <?xml> block stuck in the middle so it's not recognized as valid xml anymore.
Any ideas? Why the extra <?xml>; How to restrict template to particular parts of the tree?
Through a great deal of trial and error, I stumbled on the following solution: I added a root anchor to the third template match criteria.
Instead of match="*[count(*) > 0]", I now have /*[count(*) > 0]. This appears to eliminate the outer <items> element. If anyone can tell me why, I'd appreciate it. Why would this be different than /objects/*[count(*) > 0] ?
I do think Dimitre is right about the processor (which is IBM Cast Iron) so I did open a ticket. I tested the same stylesheet from above on an online XSLT tester and did not get the extra <?xml ?> tag.
<?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" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="*[count(*) = 0]">
<item>
<name>
<xsl:value-of select="name(.)" />
</name>
<value>
<xsl:value-of select="." />
</value>
</item>
</xsl:template>
<xsl:template match="/*[count(*) > 0]">
<items>
<xsl:apply-templates/>
</items>