0

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.

thinkster
  • 586
  • 2
  • 5
  • 19

2 Answers2

1

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>
Tim C
  • 70,053
  • 14
  • 74
  • 93
  • Yeah this would work but would this be more efficient that `xsl:choose`? Because if I am not wrong this iterates over how many ever galaxies up to 3 times (or more depending on the count) where we could match everything with a single run when using `xsl:choose`? That was the reason I thought if I could run it through a single time and remap the returned nodeset from `get-galaxy-types` using a template but I am not sure how to do it. – thinkster Jul 18 '14 at 14:25
  • Possibly... So, if you wanted to use the `xsl:choose` method you could the complicated `xsl:sort` method I showed at the beginning of my answer. – Tim C Jul 18 '14 at 17:15
  • I had a thought. Efficiency could be improved with the use of a key. I've added an example of this to my answer. – Tim C Jul 19 '14 at 08:17
  • Thanks I did a benchmark for both the methods. Its really strange both of the above methods are slower when I compare it with an XSL (which I considered to have the worst performance) which does a simple call-template for every galaxytype and the template does a `for-each` for every galaxy in it. – thinkster Jul 21 '14 at 19:03
  • Any clue why that would be? Are apply-templates considered to be slower than recursive `call-template`/`for-each`? – thinkster Jul 24 '14 at 15:43
0

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.

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Wouldn't this order `GalaxyType` in ascending fashion? Final output is required to be in a different order as mentioned above and that could change depending on the consumer's requirement. That's the reason I was considering if its possible to remap the XML nodes from the called template `get-galaxy-types`. Moreover I cannot change the source/output xmls. – thinkster Jul 17 '14 at 21:21
  • @Thinkster I was under the impression that the age types were age-dependent **and** ordered. If that's not so, you need to clarify how these are passed to the stylesheet. And will there always be exactly three of them (as it appears from your hard-coding them)? – michael.hor257k Jul 17 '14 at 21:29
  • Yep `GalaxyTypes` are age dependent but its ordered in a custom order as the consumer needs it. Age values are passed in `xsl:variable` (have updated question). These variables are hard-coded for testing but this gets set from code when invoking XSL compiler. Count will not change dynamically and would remain the same once set. – thinkster Jul 17 '14 at 21:44
  • I am still confused here: what determines the order? Is it the order of the variables as they appear in the styelsheet? Your example is ambivalent in this regard. -- I am also not sure how you think to pass the variables "*from code when invoking XSL compiler*". I believe you need to use **parameters** for that? – michael.hor257k Jul 17 '14 at 22:22
  • Yes these are passed in as parameters. I started working on it assuming that `xsl:choose` would maintain the order of returned nodes so I had made the `xsl:when` comparison in that order (sry this not in the order I wanted orginally I just modifed the XSL snippet). Since this doesn't work as the order of nodeset returned from `xsl:choose` would depend on what is matched first, I am looking for a way to define a custom order in which the nodeset retruned from `xsl:choose` would be reordered. – thinkster Jul 18 '14 at 13:39
  • Okay, but what determines the order? You seem intent not to tell us this detail ;-) – michael.hor257k Jul 18 '14 at 13:46
  • hmm the one that consumes it gives an order that it needs but that said it isn't dynamic once we defined its not changing. Say in the above example it is in the order GalaxyType 2-3-1 so is there a way I can define the order in a template to which I could re-map the nodeset returned from the template `get-galaxy-types`? – thinkster Jul 18 '14 at 14:25
  • Couldn't you define the order by writing the parameters in the order the matching galaxies should appear in the output? -- Note: IMHO, you're approaching from the wrong angle: instead of processing them first, then sorting them, you should process them in the correct order to begin with. – michael.hor257k Jul 18 '14 at 14:57
  • ok I am really not sure how to do that. It all started with this question in my mind - how can one re-order a nodeset from `for-each` `xsl-choose` (call-template). May be the word re-order is being confused with re-map. I look at it like remapping a nodeset returned from `call-template`. – thinkster Jul 18 '14 at 15:30
  • Normally, you would reorder a node-set by doing a sort. That's assuming you do have a node-set, which I am not at all sure of. – michael.hor257k Jul 18 '14 at 15:39
  • I believe `get-galaxy-types` returns a nodeset. – thinkster Jul 18 '14 at 16:15
  • No, it's a result-tree-fragment. And we are getting further and further away from the topic. I still don't know what determines the order by which the output needs to be ordered - regardless of the method which will be used. – michael.hor257k Jul 18 '14 at 16:30
  • ok I would say that the re-ordering of xml tree fragment needs to be achieved using a mapping template like Element1 - Element2, Element3 - Element1, Element2- Element3 etc. (which I am not sure how I could achieve) that could re-order the elements in a defined custom order as you define. – thinkster Jul 18 '14 at 18:11
  • I am sorry, that means nothing to me. The order is either hard-coded in the XSLT file (which means we know it now - so why would we need to mess around with mappings?) or it's going to be passed as a parameter along with the values. It seems to me you are asking about the *how* before you have figured out the *what*. – michael.hor257k Jul 18 '14 at 18:19
  • Hard-coded is what I refer to as a mapping. Once we have the xml fragment from `xsl-choose` that needs to be re-mapped in the hard-coded order. – thinkster Jul 21 '14 at 19:07
  • I think that if you review the chain of comments so far, you'll see that I keep asking what is the order you want the output to follow. I don't think you have answered this, and I am tired of playing this game. – michael.hor257k Jul 21 '14 at 19:13
  • Its there in the XSL Snippet (xsl:choose when test) as 2-3-1. You can assume any random order for `galaxytype` to explain it. – thinkster Jul 21 '14 at 19:33
  • If the sort order is *arbitrary* (not *random*) then I would do something very similar to what Tim C has suggested - except I would loop over the passed parameters, instead of hard-coding each individual `apply-templates` instruction. That would enable you to pass a varying number of parameters at runtime (although never more than a fixed, pre-determined number). – michael.hor257k Jul 21 '14 at 19:59