Ask Ben: Sorting A Sub-Tree Of An XML Document Using ColdFusion And XSLT

Posted November 24, 2008 at 9:17 AM by Ben Nadel

Tags: ColdFusion, Ask Ben

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.




Reader Comments

ike
Nov 24, 2008 at 12:28 PM // reply »
78 Comments

The XSL here doesn't need to be in a cfxml tag, you can just save it to a string with cfsavecontent, which is how I generally save xsl. I could easily see a function with an xsl template created at run-time that accepts just 2 arguments in addition to the XML to modify and return the updated XML. The first argument would be an xpath to target the nodes to sort and the 2nd argument would be an xpath to target the attributes (or other nodes) to use for sorting. So you'd say myXML = xslSort(myXML,"/root/parent/sortednode","@sortby") -- and you could make that 2nd xpath argument optional by making it default to "./text()" or possibly ".". I guess you would want a couple extra arguments though to allow reverse order.


ike
Nov 24, 2008 at 12:30 PM // reply »
78 Comments

The down side is that xmlTransform returns a string instead of modifying the existing XML packet in memory, so you have to then parse the string and then return it to the same variable name if you want the packet to stay in the same place. Would be nice imo if it worked like ArrayAppend() where the target XML packet is modified directly instead of making a string rep. of a copy of the XML.


Nov 24, 2008 at 12:33 PM // reply »
10,640 Comments

@Ike,

It's funny you mention CFSaveContent - as I was typing the actual CFXML tag, it occurred to me, "I don't have to compile this as a CF XML document." But then, I just said, "Ah" and went ahead with the CFXML anyway.

The function is a bit more complicated that I would have hoped. I was tinkering with it earlier. The problem is that the the above example uses the *parent* of the target nodes. Ideally, I think you'd want to, as you say, give XPath the target nodes. Here is what I had:

XmlSort( Xml, TargetXPath, SortXpath [, DataType [, Direction]] )

But, once I have the target XPath, I am not sure how to really move up one place (to parent) and then run the sort from there. Cause, once you are *in* a target node template, you lose the sort context.... I think. My knowledge of XPath is not quite this good.

Still tinkering :)


Nov 24, 2008 at 12:34 PM // reply »
10,640 Comments

@Ike,

Yeah, true. There would be nothing fast about this anyway - XPath is very slow in ColdFusion. So, re-parsing the string into XML is probably just a drop in the pond.


ike
Nov 24, 2008 at 1:18 PM // reply »
78 Comments

I'm pretty sure you can get the parent node with .. in an xpath statement, the same way . represents the current node -- much like file path conventions... so ../blah means go up to the parent node and find me the blah from there ... I think there's also a parent-node:: or somesuch, but I haven't really used it.


Nov 24, 2008 at 1:33 PM // reply »
10,640 Comments

@Ike,

You are correct. I am messing around with it. Fingers crossed.


Nov 24, 2008 at 1:39 PM // reply »
1 Comments

Ben,

Thanks for this. I appreciate your xslt knowledge. I can definitely use this, and I look forward to seeing what you come up with for a more generic solution.


Nov 24, 2008 at 2:38 PM // reply »
10,640 Comments

Here's my best attempt so far:

http://www.bennadel.com/index.cfm?dax=blog:1409.view

I am not totally happy with it cause I feel the XPath requirements are somewhat awkward. I'd like to come up with something better, but because XML can be so mixed, maybe this makes the most sense???


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »
Feb 9, 2012 at 10:29 PM
Learning ColdFusion 9: Application-Specific Data Sources
@Ben, No offence, but if people were really wanting advanced features they would be using a platform like ASP.NET MVC. CFML is so structurally compromised as a tag-based scripting language that ... read »
Feb 9, 2012 at 10:03 PM
Subversion - Cleanup Failed To Process The Following Paths
@Leviaguirre, do you still have problems with this? ... read »