drewich
drewich

Reputation: 148

XSLT - Copy grandchildren reordered

I'm quite new to XSLT and I was trying to copy an existent XML file I already have but with the elements reordered but I got stuck when trying to reorder grandchildren.

Let's say I have this input:

<grandParent>
    <parent> 
        <c>789</c>
        <b>
           <b2>123</b2>
           <b1>456</b1>
        </b>
        <a>123</a>
    </parent>
    ....
</grandParent>

What I want to do is get the same XML file but changing the order of the tags to be a,b,c with b = b1, b2 in that order. So I started with the XSLT file:

<xsl:template match="node()|@*">    <- This should copy everything as it is
    <xsl:copy>
           <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
</xsl:template>
<xsl:template match="grandParent/parent"> <- parent elements will copy in this order
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:copy-of select="a"/>
        <xsl:copy-of select="b"/>
        <xsl:copy-of select="c"/>
    </xsl:copy>
</xsl:template>

But "xsl:copy-of select="b"" copies the elements as they are specified (b2, b1). I tried using another xsl:template for "grandParent/parent/b" but wouldn't help.

Maybe I'm not doing things the correct way... Any tips?

Thanks!

SOLUTION - Thanks to Nils

Your solution works just fine Nils, I just customized it a bit more to fit in my current scenario where "b" is optional and the names of the tags might not be correlative. The final code is like this:

<xsl:template match="node()|@*">    <- This should copy everything as it is
    <xsl:copy>
           <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
</xsl:template>
<xsl:template match="grandParent/parent"> <- parent elements will copy in this order
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:copy-of select="a"/>
                    <xslt:if test="b">
                        <b>
                        <xsl:copy-of select="b1"/>
                        <xsl:copy-of select="b2"/>
                        </b>
                    </xslt:if>
        <xsl:copy-of select="b"/>
        <xsl:copy-of select="c"/>
    </xsl:copy>
</xsl:template>

Upvotes: 1

Views: 583

Answers (3)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243469

Here is a most generic solution -- using xsl:sort and templates -- no xsl:copy-of and no hardcoding of specific names:

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

 <xsl:template match="node()|@*">
     <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
 </xsl:template>

 <xsl:template match="*/*">
     <xsl:copy>
       <xsl:apply-templates select="@*"/>
       <xsl:apply-templates select="node()">
         <xsl:sort select="name()"/>
       </xsl:apply-templates>
     </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<grandParent>
    <parent>
        <c>789</c>
        <b>
            <b2>123</b2>
            <b1>456</b1>
        </b>
        <a>123</a>
    </parent>
    ....
</grandParent>

the wanted, correct result is produced:

<grandParent>
   <parent>
      <a>123</a>
      <b>
         <b1>456</b1>
         <b2>123</b2>
      </b>
      <c>789</c>
   </parent>
    ....
</grandParent>

Now, let's change all the names in the XML document -- note that none of the other answers works with this:

<someGrandParent>
    <someParent>
        <z>789</z>
        <y>
            <y2>123</y2>
            <y1>456</y1>
        </y>
        <x>123</x>
    </someParent>
    ....
</someGrandParent>

We apply the same transformation and it again produces the correct result:

<someGrandParent>
   <someParent>
      <x>123</x>
      <y>
         <y1>456</y1>
         <y2>123</y2>
      </y>
      <z>789</z>
   </someParent>
    ....
</someGrandParent>

Upvotes: 1

Ian Roberts
Ian Roberts

Reputation: 122364

I tried using another xsl:template for "grandParent/parent/b" but wouldn't help.

Since you have an identity template you should use <xsl:apply-templates> instead of <xsl:copy-of>

<xsl:template match="node()|@*">
    <xsl:copy>
           <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
</xsl:template>
<xsl:template match="grandParent/parent">
    <xsl:copy>
        <xsl:apply-templates select="@*"/>
        <xsl:apply-templates select="a"/>
        <xsl:apply-templates select="b"/>
        <xsl:apply-templates select="c"/>
    </xsl:copy>
</xsl:template>

Now you can add a similar template for the b elements

<xsl:template match="parent/b">
    <xsl:copy>
        <xsl:apply-templates select="@*"/>
        <xsl:apply-templates select="b1"/>
        <xsl:apply-templates select="b2"/>
    </xsl:copy>
</xsl:template>

This will nicely handle the case where b doesn't exist - if the select="b" doesn't find any elements, then no templates will fire.

In fact, if the sort order is the same in both cases (alphabetically by element name) then you can combine the two templates into one which uses <xsl:sort>, giving a complete transformation of

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:strip-space elements="*" />
  <xsl:output method="xml" indent="yes" />

  <xsl:template match="node()|@*">
    <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="grandParent/parent | parent/b">
    <xsl:copy>
      <xsl:apply-templates select="@*"/>
      <xsl:apply-templates select="*">
        <xsl:sort select="name()" />
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

(for the example XML you've given you don't actually need the @* bits because the XML doesn't include any attributes, but it won't do any harm to leave it there in case there are any in the real XML or you add any in future).

Upvotes: 1

Nils Werner
Nils Werner

Reputation: 36739

Using xsl:sort.

The following code is off the top of my head and might not work; the thought behind it should be clear though.

<xsl:template match="node()|@*">    <- This should copy everything as it is
    <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
</xsl:template>

<xsl:template match="grandParent/parent"> <- parent elements will copy in this order
    <xsl:copy>
    <xsl:copy-of select="@*"/>
    <xsl:copy-of select="a"/>
    <b>
        <xsl:for-each select="b/*">
            <xsl:sort select="text()" />
            <xsl:copy-of select="." />
        </xsl:for-each>
    </b>
    <xsl:copy-of select="c"/>
    </xsl:copy>
</xsl:template>

Upvotes: 1

Related Questions