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 best solutions below

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>