Reorder nodes returned from xsl:choose

234 views Asked by At

What would be an efficient way to reorder a group of nodes selected using xsl:choose (XSLT 1.0).

Below is the sample source XML:

<Universe>
 <CObj>
    <Galaxies>
        <Galaxy>
            <Profile>
                <Name>MilkyWay</Name>
                <Age>12.5</Age>
            </Profile>
            <PlanetarySystem>
                <Name>Solar</Name>
                <Location></Location>
                <Planet>
                    <Name>Earth</Name>
                    <Satellite>Y</Satellite>
                              ...
                              ...
                              ...
                </Planet>
                        ...
                        ...
                        ...
            </PlanetarySystem>
            <PlanetarySystem>
                        ...
                        ...
                        ...
            </PlanetarySystem>
        </Galaxy>
        <Galaxy>
                ...
                ...
                ...
        </Galaxy>
    </Galaxies>
 </CObj>
</Universe>

XSL snippet:

<xsl:template name="get-galaxy-types">
<xsl:variable name="galaxy_age1" select ="1"  />
<xsl:variable name="galaxy_age2" select ="5"  />
<xsl:variable name="galaxy_age3" select ="10"  />
<xsl:for-each select="Galaxies/Galaxy/Profile/Age">
        <xsl:choose>
            <xsl:when test=".=$galaxy_age2">
                <GalaxyType2>
                    <xsl:value-of select="../Profile/Name"/>
                </GalaxyType2>
            </xsl:when>
            <xsl:when test=".=$galaxy_age3">
                <GalaxyType3>
                    <xsl:value-of select="../Profile/Name"/>
                </GalaxyType3>
            </xsl:when>
            <xsl:when test=".=$galaxy_age1">
                <GalaxyType1>
                    <xsl:value-of select="../Profile/Name"/>
                </GalaxyType1>
            </xsl:when>
        </xsl:choose>
</xsl:for-each>

Above XSL template is called from main template like:

<xsl:template match="Universe">
    <GalaxyTypes>
        <xsl:call-template name="get-galaxy-types"/>
    </GalaxyTypes>
</xsl:template>

Output XML: Note that the order of <GalaxyType> cannot be changed.

<Universe>
...
...
...
  <GalaxyTypes>
      <GalaxyType2>xxxxxx</GalaxyType2>
      <GalaxyType3>xxxxxx</GalaxyType3>
      <GalaxyType1>xxxxxx</GalaxyType1>
  </GalaxyTypes>
...
...
...
</Universe>

Since xsl:choose returns the XML nodes as and when it finds a match I am unable to find a straight forward way to control the order in which I want GalaxyType to appear in the output XML.

How can I have a generic template to perform reordering for any elements that might get added in the future that may fall in to similar requirement. I am fine with having a remapping template within this XSL but I am not really sure how to accomplish this in a really elegant and efficient way.

2

There are 2 answers

18
michael.hor257k On

It's very difficult to navigate between the scattered snippets of your code. Still, it seems to me you should change your strategy to something like:

<xsl:template match="?">
...
    <GalaxyTypes>
        <xsl:apply-templates select="??/???/Galaxy">
            <xsl:sort select="Profile/Age" data-type="number" order="ascending"/>
        </xsl:apply-templates=>
     </GalaxyTypes>
...
</xsl:template>


<xsl:template match="Galaxy">
       <xsl:choose>
            <xsl:when test="Profile/Age=$galaxy_age1">
                <GalaxyType1>
                    <xsl:value-of select="Profile/Name"/>
                </GalaxyType1>
            </xsl:when>
            <xsl:when test="Profile/Age=$galaxy_age2">
                <GalaxyType2>
                    <xsl:value-of select="Profile/Name"/>
                </GalaxyType2>
            </xsl:when>
            <xsl:when test="Profile/Age=$galaxy_age3">
                <GalaxyType3>
                    <xsl:value-of select="Profile/Name"/>
                </GalaxyType3>
            </xsl:when>
        </xsl:choose>
</xsl:template>

--
Note that your output would be much better formatted if all galaxies were a uniform <Galaxy> element, with a type attribute to tell them apart.

5
Tim C On

I am going to guess you want to put the galaxies matching galaxy-age1 first, then the ones matching galaxy-age2 next, and finally the ones for galaxy-age3. I am also assuming the ages specified may not be in ascending order (that is to say, galaxy-age3 could be less that galaxy-age1.

To start with, it might be more natural to do your xsl:for-each over the Galaxy elements

<xsl:for-each select="Galaxies/Galaxy">

Then, to do your customisable sort, you could first define a variable like so...

<xsl:variable name="sortAges" 
              select="concat('-', $galaxy_age1, '-', $galaxy_age2, '-', $galaxy_age3, '-')" />

Note the order the parameters appear in the concat statement corresponds to the order they need to be output.

Then, your xsl:for-each could look this this...

<xsl:for-each select="Galaxies/Galaxy">
  <xsl:sort select="string-length(substring-before($sortAges, concat('-', Profile/Age, '-')))" />

But this is not very elegant. It might be better to simply have a template that matches Galaxy and use three separate xsl:apply-templates to select the Galaxy; one for each age.

Try this XSLT too

<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="galaxy_age1" select="1" />
<xsl:param name="galaxy_age2" select="5" />
<xsl:param name="galaxy_age3" select="10" />

<xsl:template match="Universe">
    <GalaxyTypes>
        <xsl:apply-templates select="Galaxies/Galaxy[Profile/Age = $galaxy_age1]">
            <xsl:with-param name="num" select="1" />
        </xsl:apply-templates>
        <xsl:apply-templates select="Galaxies/Galaxy[Profile/Age = $galaxy_age2]">
            <xsl:with-param name="num" select="2" />
        </xsl:apply-templates>
        <xsl:apply-templates select="Galaxies/Galaxy[Profile/Age = $galaxy_age3]">
            <xsl:with-param name="num" select="3" />
        </xsl:apply-templates>
    </GalaxyTypes>
</xsl:template>

<xsl:template match="Galaxy">
    <xsl:param name="num" />
    <xsl:element name="Galaxy{$num}">
        <xsl:value-of select="Profile/Name"/>
    </xsl:element>
</xsl:template>
</xsl:stylesheet>

EDIT: To make this more efficient, consider using a key to look up the Galaxy elements by their name. Try this XSLT too

<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="galaxy_age1" select="1" />
<xsl:param name="galaxy_age2" select="10" />
<xsl:param name="galaxy_age3" select="5" />

<xsl:key name="galaxy" match="Galaxy" use="Profile/Age" />

<xsl:template match="Universe">
    <GalaxyTypes>
        <xsl:apply-templates select="key('galaxy', $galaxy_age1)">
            <xsl:with-param name="num" select="1" />
        </xsl:apply-templates>
        <xsl:apply-templates select="key('galaxy', $galaxy_age2)">
            <xsl:with-param name="num" select="2" />
        </xsl:apply-templates>
        <xsl:apply-templates select="key('galaxy', $galaxy_age3)">
            <xsl:with-param name="num" select="3" />
        </xsl:apply-templates>
    </GalaxyTypes>
</xsl:template>

<xsl:template match="Galaxy">
    <xsl:param name="num" />
    <xsl:element name="Galaxy{$num}">
        <xsl:value-of select="Profile/Name"/>
    </xsl:element>
</xsl:template>
</xsl:stylesheet>