<cffunction name="XmlSort2" access="public" returntype="xml" output="true" hint="I sort part of an XML documument based on the given XPath and sort characteristics."> <!--- Define arguments. ---> <cfargument name="Xml" type="any" required="true" hint="I am an XML string or ColdFusion XML document." /> <cfargument name="XPath" type="string" required="true" hint="I am the XPath to the target node set." /> <cfargument name="SortXPath" type="any" required="false" default="text()" hint="I am the XPath value upon which the sort is being conducted. This can be a string or an array (if multiple sorting options are required)." /> <cfargument name="Direction" type="string" required="false" default="ASC" hint="I am the sort direction. ASC or DESC." /> <!--- Define the local scope. ---> <cfset var LOCAL = {} /> <!--- Check to see if our XML document is an Xml document. If is not, then we want to convert it into a true ColdFusion XML document. ---> <cfif NOT IsXmlDoc( ARGUMENTS.Xml )> <!--- Convert to an XML document. ---> <cfset ARGUMENTS.Xml = XmlParse( Trim( ARGUMENTS.Xml ) ) /> </cfif> <!--- Check to see if the given sorting option is a string or an array. If it's a string, then let's convert it to an array so that we can treat it uniformily later on. ---> <cfif IsSimpleValue( ARGUMENTS.SortXPath )> <!--- We need to copy this to get around a bug in the way ColdFusion handles implicit array creation involving its own values. ---> <cfset LOCAL.SortCopy = ARGUMENTS.SortXPath /> <!--- Convert simple value to an array. ---> <cfset ARGUMENTS.SortXPath = [ LOCAL.SortCopy ] /> </cfif> <!--- Get the set of target nodes. ---> <cfset LOCAL.TargetNodes = XmlSearch( ARGUMENTS.Xml, ARGUMENTS.XPath ) /> <!--- Check to make sure we have target nodes. If not, then just return the original XML. ---> <cfif (ArrayLen( LOCAL.TargetNodes ) LT 2)> <cfreturn ARGUMENTS.Xml /> </cfif> <!--- ASSERT: At this point, we know that we have a valid set of target nodes that can be sorted. ---> <!--- Perform bubble sort on target nodes. ---> <cfloop index="LOCAL.EndIndex" from="#(ArrayLen( LOCAL.TargetNodes ) - 1)#" to="1" step="-1"> <!--- Loop over nodes starting at 1 and then proceeding until we reach the "end index". This way, we will not duplicate our comparison of the last value. ---> <cfloop index="LOCAL.Index" from="1" to="#LOCAL.EndIndex#" step="1"> <!--- Get the two target nodes. ---> <cfset LOCAL.NodeOne = LOCAL.TargetNodes[ LOCAL.Index ] /> <cfset LOCAL.NodeTwo = LOCAL.TargetNodes[ LOCAL.Index + 1 ] /> <!--- Now that we have the two target nodes, we have to sort them based on the array of comparison values. We only need to proceed through the comparisons if the previous comparison is equal. ---> <cfloop index="LOCAL.SortXPath" array="#ARGUMENTS.SortXPath#"> <!--- Get the comparison value for given node. ---> <cfset LOCAL.ValueOne = XmlSearch( LOCAL.NodeOne, LOCAL.SortXPath ) /> <!--- Get the comparison value for next node. ---> <cfset LOCAL.ValueTwo = XmlSearch( LOCAL.NodeTwo, LOCAL.SortXPath ) /> <!--- Now, we have to get our values down to something that is usable. If this is a node, then get the text value. If this is an attribute then get the attribute value. ---> <cfif StructKeyExists( LOCAL.ValueOne[ 1 ], "XmlText" )> <!--- Get node text. ---> <cfset LOCAL.ValueOne = LOCAL.ValueOne[ 1 ].XmlText /> <cfset LOCAL.ValueTwo = LOCAL.ValueTwo[ 1 ].XmlText /> <cfelse> <!--- Get attribute value. ---> <cfset LOCAL.ValueOne = LOCAL.ValueOne[ 1 ].XmlValue /> <cfset LOCAL.ValueTwo = LOCAL.ValueTwo[ 1 ].XmlValue /> </cfif> <!--- Check to see if these two values are equal. If they are, then we just want to proceed to the next sorting condition. It is only if they are unequal that we can take action. ---> <cfif (LOCAL.ValueOne NEQ LOCAL.ValueTwo)> <!--- Check to see which direction we are sorting in. ---> <cfif ( ( (ARGUMENTS.Direction EQ "ASC") AND (LOCAL.ValueOne GT LOCAL.ValueTwo) ) OR ( (ARGUMENTS.Direction EQ "DESC") AND (LOCAL.ValueOne LT LOCAL.ValueTwo) ))> <!--- Swap nodes. ---> <cfset ArraySwap( LOCAL.TargetNodes, LOCAL.Index, (LOCAL.Index + 1) ) /> </cfif> <!--- Break out of the comparison sub-loop since we found two values which are not equal. ---> <cfbreak /> </cfif> </cfloop> </cfloop> <!--- END: Comparison loop. ---> </cfloop> <!--- END: Outer loop. ---> <!--- ASSERT: At this point our disconnected set of target nodes is in the appropriate sort order. ---> <!--- Before we do anything, we need to set up a unique ID for each XML node. This way, as we go through and compare them, we can make unique comparisons. This is utilitarian and will have to be removed afterwards. ---> <cfloop index="LOCAL.TargetNode" array="#LOCAL.TargetNodes#"> <!--- Set unique ID. Use a function name space to make sure we don't have have and attribute conflicts. ---> <cfset LOCAL.TargetNode.XmlAttributes[ "XmlSort:UUID" ] = CreateUUID() /> </cfloop> <!--- Now, it's time to take that set of nodes and apply the order to the XML document. Because we don't know the placement within the existing document, we need to iterate over the parent node and check for matches. ---> <cfset LOCAL.ParentNode = XmlSearch( LOCAL.TargetNodes[ 1 ], ".." ) /> <!--- Get the parent node from the returned node set. ---> <cfset LOCAL.ParentNode = LOCAL.ParentNode[ 1 ] /> <!--- As we replace our children, we need to keep a pointer to the index of the child we are replacing INTO the existing document. As we go along, this will help us to keep track since we don't know what order the matching nodes are going to show up in. ---> <cfset LOCAL.ReplaceIndex = 1 /> <!--- Loop over the xml children. ---> <cfloop index="LOCAL.ChildIndex" from="1" to="#ArrayLen( LOCAL.ParentNode.XmlChildren )#" step="1"> <!--- Check to see if this node is one of the nodes in our target node set. ---> <cfloop index="LOCAL.TargetNode" array="#LOCAL.TargetNodes#"> <!--- Check to see if this target node is the child node that we are currently examining. When comparing, make sure that the child node actually has the sorting UUID. ---> <cfif ( StructKeyExists( LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ].XmlAttributes, "XmlSort:UUID" ) AND (LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ].XmlAttributes[ "XmlSort:UUID" ] EQ LOCAL.TargetNode.XmlAttributes[ "XmlSort:UUID" ]) )> <!--- The current child IS in our target node set. That means that we can replace in one of our target nodes in this child index. Use the current index of replacement to select the target node. Use the post-incrementer to make sure that the replce index is upped after we copy over the taret node. Use Duplicate() so that we don't mess up our node references and accidentally delete one of the nodes from the parent document. ---> <cfset LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ] = Duplicate( LOCAL.TargetNodes[ LOCAL.ReplaceIndex++ ] ) /> <!--- Remove the UUID. ---> <cfset StructDelete( LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ].XmlAttributes, "XmlSort:UUID" ) /> <!--- We matched a node - no need to keep checking for a match in our taret node set (there should be a one-to-one match). ---> <cfbreak /> </cfif> </cfloop> </cfloop> <!--- Return the updated XML document. ---> <cfreturn ARGUMENTS.Xml /> </cffunction>