Ben, I know that you have done a fair bit of work with xml files in ColdFusion and I am hoping you can help me with a problem I am trying to solve. I am interested in sorting an xml file by a given attribute. For Example:
<xmlitems>
<xmlitem name="b">something</xmlitem>
<xmlitem name="a">somethingelse</xmlitem>
</xmlitems>
I would like to sort the xmlitem nodes by the name attribute. Do you know of a simple way to do this? Can xslt be used to solve this? I know that xslt can be used for sorting, but I am not familiar with it.
This was a really interesting question. I have to say, it's not one that I have come up against before, even in my own programming, and it took me a little while to figure out. The solution I have is for this very specific problem; however, after this, I'd love to come up with a way to abstract this out into something more generic. But, that's a whole other blog post altogether.
Ok, so let's just take a look at the code, and then we can walk through it a bit more:
- <!--- Define the XML data. --->
- <cfxml variable="xmlData">
-
- <xmlitems>
- <xmlitem name="b">something</xmlitem>
- <xmlitem name="a">somethingelse</xmlitem>
- </xmlitems>
-
- </cfxml>
-
-
- <!--- Define the XSL Transofrm data. --->
- <cfxml variable="xmlTransform">
-
- <!--- Document type declaration. --->
- <?xml version="1.0" encoding="ISO-8859-1"?>
-
- <xsl:transform
- version="1.0"
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
-
- <!--- Match the root node (xmlitems). --->
- <xsl:template match="xmlitems">
-
- <!---
- Copy the current node's top-level values
- (the tag and it's attributes, but not it's
- descendents).
- --->
- <xsl:copy>
-
- <!--- Loop over the xmlitem nodes. --->
- <xsl:for-each select="xmlitem">
-
- <!---
- As we are looping over these xmlitem
- nodes, sort them ascendingly according
- to their NAME attribute.
- --->
- <xsl:sort
- select="@name"
- data-type="text"
- order="ascending"
- />
-
- <!---
- Copy the entire node (include its
- descendantas).
- --->
- <xsl:copy-of select="." />
-
- </xsl:for-each>
-
- </xsl:copy>
-
- </xsl:template>
-
- </xsl:transform>
-
- </cfxml>
-
-
- <cfoutput>
-
- <!--- Output the transformation. --->
- #HTMLEditFormat(
- XmlTransform( xmlData, xmlTransform )
- )#
-
- </cfoutput>
Running the above code, we get the following output (based on our resultant XML document):
- <?xml version="1.0" encoding="UTF-8"?>
- <xmlitems>
- <xmlitem name="a">somethingelse</xmlitem>
- <xmlitem name="b">something</xmlitem>
- </xmlitems>
As you can see, the XML document is copied over with the XmlItem nodes sorting ascendingly using the NAME attribute.
Ok, so let's walk through the code a bit. The first thing we are doing is matching a template on the root element, XmlItems. Once we have that matched node, we are copying the that node using the xsl:copy command. The xsl:copy command copies the current node without its child nodes. Within this copy, we then loop over the child nodes, XmlItem, and copy those using the xsl:copy-of command. The difference between xsl:copy and the xsl:copy-of command is that the xsl:copy-of copies over the current tag attribute AND the child nodes. The key to this solution is that inside the node loop, xsl:for-each, we are using an xsl:sort command to define the order in which the child-nodes are being iterated. In our case, the sort direction depends on the value of the NAME attribute as defined by the select="@name" sort.
I know that this solution is very specific to your sample data; but, maybe it will point you in the right direction. I'd like to now take some time to figure out how to do this in a more generic way.