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 »
11,238 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 »
11,238 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 »
11,238 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 »
11,238 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
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools