XSLT / conditional for-each with same parent (uncle) node value - xslt

I need some help understanding how I can select only certain nodes having same parent (or uncle) element value. My XML looks like this
<shipment>
<goodslines>
<goodsline>
<position>1</position>
<packagenumbers>
<packagenumber>123</packagenumber>
</packagenumbers>
</goodsline>
<goodsline>
<position>1</position>
<packagenumbers>
<packagenumber>456</packagenumbers>
</packagenumbers>
</goodsline>
<goodsline>
<position>2</position>
<packagenumbers>
<packagenumber>789</packagenumbers>
</packagenumbers>
</goodsline>
</goodslines>
</shipment>
and the desired output would be:
123,456
789
So I would need to do for-each to "packagenumber" - level so, that it would take in consideration the "position" - element from upper level
The XSL might be something like this?
<xsl:for-each select="shipment/goodslines/goodsline[some condition here?]/packagenumbers/packagenumber">
<xsl:value-of select="current()"/>
<xsl:if test="not(position() = last())">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:text>
</xsl:text>
</xsl:for-each>
Any help would be appreciated.

If your xml always has the same structure you can use this:
<xsl:template match="goodsline[position = preceding-sibling::goodsline[1]/position]">
<xsl:text>,</xsl:text>
<xsl:value-of select="packagenumbers/packagenumber"/>
</xsl:template>
<xsl:template match="goodsline">
<xsl:text>
</xsl:text>
<xsl:value-of select="packagenumbers/packagenumber"/>
</xsl:template>
More specific templates (the one with the condition) will always hit first and have higher priority. There is probably another simple solution with for-each-group.

Related

xsl:if evaluation with nmap output

For reference I am using Nmap 7.8, and I am attempting to format the output using an xsl file. Within the xsl file I call a second rules.xml file to do comparisons and add supplemental information. This is the structure of rules.xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<Item id="1">
<info>Rule Information</info>
<port>123</port>
<port>321</port>
</Item>
</root>
When I attempt to print the output of an nmap port with <xsl:value-of select="#portid"/> it works, however if I do an if, or choose/when operation it fails. When I do a static comparison <xsl:if test="port = 321"> it works with either port value.
<xsl:for-each select="ports/port">
<xsl:choose>
<xsl:when test="state/#state = 'open'">
<xsl:variable name="rules" select="document('rules.xml')"/>
<xsl:for-each select="$rules/root/Item">
<xsl:if test="port = #portid">
<xsl:value-of select="info"/>
</xsl:if>
</xsl:for-each>
</xsl:when>
</xsl:choose>
</xsl:for-each>
The desired behavior is to loop through all ports within the nmap output and add the additional info tags in an Item which contains a matching port tag. The mapping is 1 Item to Many ports, so I would like to avoid indexing rules.xml with ports at the top level to prevent duplicate .
When using expressions like port = #portId you need to think about what the context item is. There's no element that has both a child port and an attribute portId, so it looks like the two sides of this expression are expecting different context items, which of course can't happen.
Generally when you're doing joins between several documents (or within a single document) you need to bind variables to nodes so that you can make explicit selections rather than relying on implicit context.
I am guessing you want to do:
<xsl:for-each select="ports/port">
<xsl:choose>
<xsl:when test="state/#state = 'open'">
<xsl:variable name="portid" select="#portid"/>
<xsl:variable name="rules" select="document('rules.xml')"/>
<xsl:for-each select="$rules/root/Item">
<xsl:if test="port = $portid">
<xsl:value-of select="info"/>
</xsl:if>
</xsl:for-each>
</xsl:when>
</xsl:choose>
</xsl:for-each>
or perhaps more succinctly:
<xsl:variable name="rules" select="document('rules.xml')"/>
<xsl:for-each select="ports/port[state/#state = 'open']">
<xsl:variable name="portid" select="#portid"/>
<xsl:for-each select="$rules/root/Item[port = $portid]">
<xsl:value-of select="info"/>
</xsl:for-each>
</xsl:for-each>
What you have now is looking for Item that has a child port whose value is equal to the value of the portid attribute of the parent Item.

Selecting the following sibling of a sorted node

Trying to work out how to select the following-sibling of an XSLT node when the node has been sorted in XSLT 1.0. I've searched but can't find anything for sorted nodes, as it only selects the sibling of the unsorted node.
Data
<data>
<number order='4'>Four</number>
<number order='1'>One</number>
<number order='3'>Three</number>
<number order='2'>Two</number>
</data>
Code
<xsl:for-each select="/data/number">
<xsl:sort select="#order"/>
<xsl:if test="position() mod 2 = 1">
<xsl:value-of select="text()"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="following-sibling::*/text()"/>
</xsl:if>
</xsl:for-each>
Expected output
One - Two
Three - Four
Actual Output
One - Three
Three - Two
When you sort a sequence of nodes, you get the same nodes in a new sequence. Because they are the same nodes, they have the same siblings that they always had. If you copy the nodes to a result tree, then the copies will have new siblings, but that's because of the action of writing them to a result tree, not because of the sorting action.
Another way of putting this: you are processing a sequence of nodes that aren't siblings, so you can't use following-sibling to get the next node in the sequence.
Processing a sorted sequence of nodes becomes much easier in XSLT 2.0, which allows such a sequence to be bound to a variable. XSLT 1.0 only has node-sets, so sequences of nodes in a particular order can only exist transiently.
But in this particular case, it seems easy enough to do
<xsl:value-of select="."/>
<xsl:if test="position() mod 2 = 1">
<xsl:text> - </xsl:text>
</xsl:if>
it only selects the sibling of the unsorted node.
That is correct. Why don't you do simply:
<xsl:for-each select="/data/number">
<xsl:sort select="#order"/>
<xsl:value-of select="."/>
<xsl:choose>
<xsl:when test="position() mod 2 = 1">
<xsl:text>-</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>
</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
Note that the default sort data-type is text; you probably want to make it:
<xsl:sort select="#order" data-type="number"/>

XSLT 1.0: if statement when position matches a variable value

I'm trying to insert an IF statement into an existing FOR-EACH loop to do something slightly different if it matches a variable from another node (the node I want is actually a sibling of it's parent - if that makes sense!?).
The value is a simple integer. I basically want to say: If the position is equal to the variable number then do XXXX.
Here is the XSLT, it's only v1.0 and not 2.0 that I can use.
<xsl:for-each select="/Properties/Data/Datum[#ID='ID1']/DCR[#Type='accordion_tab']/accordion_tab/sections">
<h3 class="accordionButton">
<xsl:if test="position()='openpane value to go here'">
<xsl:attribute name="class">
<xsl:text>new text</xsl:text>
</xsl:attribute>
</xsl:if>
</xsl:for-each>
My XML extract is here:
<sections>
<title>title</title>
<text>some text</text>
</sections>
<openpane>2</openpane>
You didn't make this clear in your question, but I assume you iterate over the sections elements in your for-each loop. From the for-each loop you can reach the openpane element by going through the parent of the current sections element:
<xsl:for-each select="sections">
<xsl:if test="position() = ../openpane">
...
</xsl:if>
</xsl:for-each>
You could also define a variable referring to the openpane element first:
<xsl:variable name="openpane" select="openpane"/>
<xsl:for-each select="sections">
<xsl:if test="position() = $openpane">
...
</xsl:if>
</xsl:for-each>

How do I render a comma delimited list using xsl:for-each

I am rendering a list of tickers to html via xslt and I would like for the list to be comma deliimited. Assuming I was going to use xsl:for-each...
<xsl:for-each select="/Tickers/Ticker">
<xsl:value-of select="TickerSymbol"/>,
</xsl:for-each>
What is the best way to get rid of the trailing comma? Is there something better than xsl:for-each?
<xsl:for-each select="/Tickers/Ticker">
<xsl:if test="position() > 1">, </xsl:if>
<xsl:value-of select="TickerSymbol"/>
</xsl:for-each>
In XSLT 2.0 you could do it (without a for-each) using the string-join function:
<xsl:value-of select="string-join(/Tickers/Ticker, ',')"/>
In XSLT 1.0, another alternative to using xsl:for-each would be to use xsl:apply-templates
<xsl:template match="/">
<!-- Output first element without a preceding comma -->
<xsl:apply-templates select="/Tickers/Ticker[position()=1]" />
<!-- Output subsequent elements with a preceding comma -->
<xsl:apply-templates select="/Tickers/Ticker[position()>1]">
<xsl:with-param name="separator">,</xsl:with-param>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Ticker">
<xsl:param name="separator" />
<xsl:value-of select="$separator" /><xsl:value-of select="TickerSymbol" />
</xsl:template>
I know you said xsl 2.0 is not an option and it has been a long time since the question was asked, but for all those searching for a posibility to do what you wanted to achieve:
There is an easier way in xsl 2.0 or higher
<xsl:value-of separator=", " select="/Tickers/Ticker/TickerSymbol" />
This will read your /Tickers/Ticker elements and insert ', ' as separator where needed
If there is an easier way to do this I am looking forward for advice
Regards Kevin

Match conditionally upon current node value

Given the following XML:
<current>
<login_name>jd</login_name>
</current>
<people>
<person>
<first>John</first>
<last>Doe</last>
<login_name>jd</login_name>
</preson>
<person>
<first>Pierre</first>
<last>Spring</last>
<login_name>ps</login_name>
</preson>
</people>
How can I get "John Doe" from within the current/login matcher?
I tried the following:
<xsl:template match="current/login_name">
<xsl:value-of select="../people/first[login_name = .]"/>
<xsl:text> </xsl:text>
<xsl:value-of select="../people/last[login_name = .]"/>
</xsl:template>
I'd define a key to index the people:
<xsl:key name="people" match="person" use="login_name" />
Using a key here simply keeps the code clean, but you might also find it helpful for efficiency if you're often having to retrieve the <person> elements based on their <login_name> child.
I'd have a template that returned the formatted name of a given <person>:
<xsl:template match="person" mode="name">
<xsl:value-of select="concat(first, ' ', last)" />
</xsl:template>
And then I'd do:
<xsl:template match="current/login_name">
<xsl:apply-templates select="key('people', .)" mode="name" />
</xsl:template>
You want current() function
<xsl:template match="current/login_name">
<xsl:value-of select="../../people/person[login_name = current()]/first"/>
<xsl:text> </xsl:text>
<xsl:value-of select="../../people/person[login_name = current()]/last"/>
</xsl:template>
or a bit more cleaner:
<xsl:template match="current/login_name">
<xsl:for-each select="../../people/person[login_name = current()]">
<xsl:value-of select="first"/>
<xsl:text> </xsl:text>
<xsl:value-of select="last"/>
</xsl:for-each>
</xsl:template>
If you need to access multiple users, then JeniT's <xsl:key /> approach is ideal.
Here is my alternative take on it:
<xsl:template match="current/login_name">
<xsl:variable name="person" select="//people/person[login_name = .]" />
<xsl:value-of select="concat($person/first, ' ', $person/last)" />
</xsl:template>
We assign the selected <person> node to a variable, then we use the concat() function to output the first/last names.
There is also an error in your example XML. The <person> node incorrectly ends with </preson> (typo)
A better solution could be given if we knew the overall structure of the XML document (with root nodes, etc.)
I think what he actually wanted was the replacement in the match for the "current" node, not a match in the person node:
<xsl:variable name="login" select="//current/login_name/text()"/>
<xsl:template match="current/login_name">
<xsl:value-of select='concat(../../people/person[login_name=$login]/first," ", ../../people/person[login_name=$login]/last)'/>
</xsl:template>
Just to add my thoughts to the stack
<xsl:template match="login_name[parent::current]">
<xsl:variable name="login" select="text()"/>
<xsl:value-of select='concat(ancestor::people/child::person[login_name=$login]/child::first/text()," ",ancestor::people/child::person[login_name=$login]/child::last/text())'/>
</xsl:template>
I always prefer to use the axes explicitly in my XPath, more verbose but clearer IMHO.
Depending on how the rest of the XML documents looks (assuming this is just a fragment) you might need to constrain the reference to "ancestor::people" for example using "ancestor::people[1]" to constrain to the first people ancestor.