rpg
rpg

Reputation: 1652

XSLT 1.0 - conditional node assignment

using pure XSLT 1.0, how can I conditionally assign the node. I am trying something like this but it's not working.

<xsl:variable name="topcall" select="//topcall"/>
<xsl:variable name="focusedcall" select="//focusedcall" />

<xsl:variable name="firstcall" select="$topcall | $focusedcall"/>

For variable firstcall, I am doing the conditional node selection. if there is a topcall then assign it to firstcall, othersie assign firstcall to the focusedcall.

Upvotes: 5

Views: 2415

Answers (3)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243479

I. XSLT 1.0 Solution This short (30 lines), simple and parameterized transformation works with any number of node types/names:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pRatedCalls">
   <call type="topcall"/>
   <call type="focusedcall"/>
   <call type="normalcall"/>
 </xsl:param>

 <xsl:variable name="vRatedCalls" select=
  "document('')/*/xsl:param[@name='pRatedCalls']/*"/>

 <xsl:variable name="vDoc" select="/"/>

 <xsl:variable name="vpresentCallNames">
  <xsl:for-each select="$vRatedCalls">
   <xsl:value-of select=
   "name($vDoc//*[name()=current()/@type][1])"/>
   <xsl:text> </xsl:text>
  </xsl:for-each>
 </xsl:variable>

 <xsl:template match="/">
  <xsl:copy-of select=
   "//*[name()
       =
        substring-before(normalize-space($vpresentCallNames),' ')]"/>
 </xsl:template>
</xsl:stylesheet>

When applied to this XML document (do note the document order doesn't coincide with the specified priorities in the pRatedCalls parameter):

<t>
 <normalcall/>
 <focusedcall/>
 <topcall/>
</t>

produces exactly the wanted, correct result:

<topcall/>

when the same transformation is applied to the following XML document:

<t>
 <normalcall/>
 <focusedcall/>
</t>

again the wanted and correct result is produced:

<focusedcall/>

Explanation:

  1. The names of the nodes that are to be searched for (as many as needed and in order of priority) are specified by the global (typically externally specified) parameter named $pRatedCalls.

  2. Within the body of the variable $vpresentCallNames we generate a space-separated list of names of elements that are both specified as a value of the type attribute of a call elementin the$pRatedCalls` parameter and also are names of elements in the XML document.

  3. Finally, we determine the first such name in this space-separated list and select all elements in the document, that have this name.

II. XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pRatedCalls" select=
  "'topcall', 'focusedcall', 'normalcall'"/>

 <xsl:template match="/">
  <xsl:sequence select=
   "//*
     [name()=$pRatedCalls
                [. = current()//*/name()]
                                        [1]
     ]"/>
 </xsl:template>
</xsl:stylesheet>

Upvotes: 2

LarsH
LarsH

Reputation: 27994

This should work:

<xsl:variable name="firstcall" select="$topcall[$topcall] |
                                       $focusedcall[not($topcall)]" />

In other words, select $topcall if $topcall nodeset is non-empty; $focusedcall if $topcall nodeset is empty.

Re-Update regarding "it can be 5-6 nodes":

Given that there may be 5-6 alternatives, i.e. 3-4 more besides $topcall and $focusedcall...

The easiest solution is to use <xsl:choose>:

<xsl:variable name="firstcall">
  <xsl:choose>
    <xsl:when test="$topcall">    <xsl:copy-of select="$topcall" /></xsl:when>
    <xsl:when test="$focusedcall"><xsl:copy-of select="$focusedcall" /></xsl:when>
    <xsl:when test="$thiscall">   <xsl:copy-of select="$thiscall" /></xsl:when>
    <xsl:otherwise>               <xsl:copy-of select="$thatcall" /></xsl:otherwise>
  </xsl:choose>
</xsl:variable>

However, in XSLT 1.0, this will convert the output of the chosen result to a result tree fragment (RTF: basically, a frozen XML subtree). After that, you won't be able to use any significant XPath expressions on $firstcall to select things from it. If you need to do XPath selections on $firstcall later, e.g. select="$firstcall[1]", you then have a few options...

  1. Put those selections into the <xsl:when> or <xsl:otherwise> so that they happen before the data gets converted to an RTF. Or,
  2. Consider the node-set() extension, which converts an RTF to a nodeset, so you can do normal XPath selections from it. This extension is available in most XSLT processors but not all. Or,
  3. Consider using XSLT 2.0, where RTFs are not an issue at all. In fact, in XPath 2.0 you can put normal if/then/else conditionals inside the XPath expression if you want to.
  4. Implement it in XPath 1.0, using nested predicates like

:

select="$topcall[$topcall] |
        ($focusedcall[$focusedcall] | $thiscall[not($focusedcall)])[not($topcall)]"

and keep on nesting as deep as necessary. In other words, here I took the XPath expression for 2 alternatives above, and replaced $focusedcall with

($focusedcall[$focusedcall] | $thiscall[not($focusedcall)])

The next iteration, you would replace $thiscall with

($thiscall[$thiscall] | $thatcall[not($thiscall)])

etc.

Of course this becomes hard to read, and error-prone, so I would not choose this option unless the others aren't feasible.

Upvotes: 5

Martin Honnen
Martin Honnen

Reputation: 167571

Does <xsl:variable name="firstcall" select="($topcall | $focusedcall)[1]"/> do what you want? That is usually the way to take the first node in document order of different types of nodes.

Upvotes: 3

Related Questions