Sort XML to XML using XSLT

13,417

Your problems all lie in this matching template here

<xsl:template match="/">
    <xsl:apply-templates>
        <xsl:sort select="/ns:Root/ns:element1/@id" />
        <xsl:copy-of select="." />
    </xsl:apply-templates>
</xsl:template>

Firstly the \ symbol matches the document level element, which is not the same as the root ns:Root, but one level about it. What this means is that when you do <xsl:apply-templates> all that will select is the ns:root element, of which there is only one, and so no point sorting it!

What you probably need to start of with is to match on the root element like so, copy it and then start sorting the children.

<xsl:template match="/*">
    <xsl:copy>
        <!-- Code to select and sort childrens -->
    </xsl:copy>
</xsl:template>

The next problem you have is with the sort statement. You are using the xpath expression /ns:Root/ns:element1/@id, but this is an absolute path, not a relative one, so will only ever pick up the @id attribute of the first ns:element1 in the document.

Assuming you were positioned on the root element already, and assuming it only had ns:element1 elements as children, you could just this

    <xsl:apply-templates>
        <xsl:sort select="@id" />
    </xsl:apply-templates>

The final problem you have is that you have an <xsl:copy-of select="." /> statement in your xsl:apply-templates which is not allowed. You probably should be using xsl:copy here, as shown above.

It is also worth pointing out, if you weren't aware already, that it is better to use the XSLT identity transform to copy existing elements, unless you want to change them in some way. That way you don't have to create templates for each particular type of element.

Try the following XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:ns="urn:TestNamespace"                >
    <xsl:output indent="yes" />

    <xsl:strip-space elements="*"/>

    <xsl:template match="text()[not(string-length(normalize-space()))]"/>

    <xsl:template match="/*">
        <xsl:copy>
        <xsl:apply-templates select="@*" />
        <xsl:apply-templates>
            <xsl:sort select="@id" />
        </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

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

When applied to your XML, the following is output

<ns:Root xmlns:ns="urn:Test.Namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="urn:Test.Namespace Test1.xsd">
   <ns:element1 id="001">
      <ns:element2 id="001.1" order="1">
         <ns:element3 id="001.1.1"/>
      </ns:element2>
      <ns:element2 id="001.2" order="2">
         <ns:element3 id="001.1.2"/>
      </ns:element2>
   </ns:element1>
   <ns:element1 id="002">
      <ns:element2 id="002.1" order="3">
         <ns:element3 id="002.1.1"/>
      </ns:element2>
      <ns:element2 id="002.2" order="4">
         <ns:element3 id="002.1.2"/>
      </ns:element2>
   </ns:element1>
   <ns:element1 id="003">
      <ns:element2 id="007.0" order="1">
         <ns:element3 id="007.1.1"/>
      </ns:element2>
   </ns:element1>
</ns:Root>
Share:
13,417
Nerdio
Author by

Nerdio

Updated on June 04, 2022

Comments

  • Nerdio
    Nerdio about 2 years

    I have a found a few similar questions to this, but struggled to 'bend' the solution to what I need, so apologies for asking again.

    I have some XML like this:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <ns:Root
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:ns="urn:Test.Namespace"  
        xsi:schemaLocation="urn:Test.Namespace Test1.xsd"
        >
        <ns:element1 id="001">
            <ns:element2 id="001.1" order="1">
                <ns:element3 id="001.1.1" />
            </ns:element2>
            <ns:element2 id="001.2" order="2">
                <ns:element3 id="001.1.2" />
            </ns:element2>        
        </ns:element1>
        <ns:element1 id="003">
            <ns:element2 id="007.0" order="1">
                <ns:element3 id="007.1.1" />
            </ns:element2>
        </ns:element1>
        <ns:element1 id="002">
            <ns:element2 id="002.1" order="3">
                <ns:element3 id="002.1.1" />
            </ns:element2>
            <ns:element2 id="002.2" order="4">
                <ns:element3 id="002.1.2" />
            </ns:element2> 
        </ns:element1>    
    </ns:Root>
    

    I have written this XSLT:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0"
                    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                    xmlns:ns="urn:Test.Namespace"
                    >
        <xsl:output indent="no" />
        <xsl:template match="text()[not(string-length(normalize-space()))]"/>
        <xsl:strip-space elements="*"/>
    
        <xsl:template match="/">
            <xsl:apply-templates>
                <xsl:sort select="/ns:Root/ns:element1/@id" />
                <xsl:copy-of select="." />
            </xsl:apply-templates>
        </xsl:template>
    
        <xsl:template match="ns:element1">
            <xsl:copy-of select="." />
            <xsl:apply-templates />        
        </xsl:template>
    
        <xsl:template match="ns:element2">
            <xsl:copy-of select="." />
            <xsl:apply-templates />
        </xsl:template>
    
        <xsl:template match="ns:element3">
            <xsl:copy-of select="." />
        </xsl:template>
    
    </xsl:stylesheet>
    

    (I cribbed the outline for this from here how to sort xml?)

    What I want to be able to do, is use this XSLT to sort my original XML by the id attribute of element1 and produce XML. The idea being that once it is sorted, I can process it with some other XSLT to get the final result.

    Unfortunately, this does not give me any output, which makes me think there is a really stupid typo somewhere, but I cannot see it.

  • Nerdio
    Nerdio about 11 years
    I have updated my XML to include the corrected Namespace (see above). I now get output, but not sorted.
  • Nerdio
    Nerdio about 11 years
    This works just fine as you have illustrated above, and I have now proven by re-working for my application. Thanks. I have a little more to learn yet :)
  • Nerdio
    Nerdio about 11 years
    Sorry, didn't realise at the time, but my 'stripped down' example here is not like my real application in that ns:root has additional attributes other than those shown. When I sort using this, these attributes are lost.
  • Nerdio
    Nerdio about 11 years
    Being 'noob' TO XML I am not sure how all of this solution works. From what I can make out this is the XSLT that does most of the reproduction of the XML <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()"/> </xsl:copy> </xsl:template> and the match attribute seems to be matching any attribute or node, but it does not seem to apply to the root. What do I need to do to get any additional attributes in ns:root to be copied out?
  • Tim C
    Tim C about 11 years
    My apologies. I missed out a line in my XSLT. It is the <xsl:template match="/*"> that specifically copies the root element here (The other template matches all other elements). I have added the line that copies across attributes too.
  • Nerdio
    Nerdio about 11 years
    No apology required, it wasn't in the original question. Thanks, this works a treat.
  • Robinho
    Robinho over 8 years
    It's possible to sort by a element ? For example: ` <?xml version="1.0" encoding="UTF-8"?> <hammer-objects> <Document> <!-- a lot of info ... --> <Attachment> <title>title 3</title> </Attachment> <Attachment> <title>title 2</title> </Attachment> <Attachment> <title>title 1</title> </Attachment> <Comment> <title>title 1</title> </Comment> <Comment> <title>title 3</title> </Comment> </Document> <hammer-objects> ` How to sort all attachments and comments by the title
  • Mister Lucky
    Mister Lucky over 8 years
    <xsl:template match="/"> <xsl:apply-templates select="hammer-objects/Document"> <xsl:sort select="*/title" /> </xsl:apply-templates> </xsl:template>