Ask Ben: Getting XML Values And Aggregates In ColdFusion

Posted August 27, 2007 at 7:00 AM by Ben Nadel

Tags: ColdFusion, Ask Ben

If you have an XML Doc:

<employees>
..<employee>
....<empid>123</empid>
..</employee>
..<employee>
....<empid>456</empid>
..</employee>
</employees>

I want a list of empid values. I can use XMLSearch to get an array of empids, and loop over it to listappend the values, but I dont want to have to loop over the nodes. I really wanted to use StructFindKey(myxml,"empid","all") but that function cant be run against xml objects. Know of a low-intensive way to get a list of empids?

I feel your pain. I wish that ColdFusion's XmlSearch() function didn't always return node structures. The problem is that returning a non-node structure is not useful at all for further XmlSearch() calls since the returned values lose all XML DOM context. In fact, they wouldn't even be part of the XML document object at all (since simple values are copied by value, not reference).

That being said, the key to making something like this "low-intensive" is by encapsulating the functionality in such a way that it is both powerful and very easy to utilize. To help you out, or at least give you some ideas, I have created a little ColdFusion component called XmlUtility.cfc. This ColdFusion component utility has two functions:

XmlUtility :: GetValueArray( XML, XPath [, NumericOnly ] )

This function takes your ColdFusion XML document and your XPath value and returns an array of the values of the nodes that get matched. If an element node is returned, this function grabs the XmlText. If an attribute node is returned, this function grabs the XmlValue. There is also a third, optional argument which will limit the returned values to be numeric (this is used primariliy by the other method of this ColdFusion component).

XmlUtility :: GetValueAggregate( XML, XPath, Aggregate )

This function takes your ColdFusion XML document, your XPath value, and returns the given aggregate of the generated value array. The available aggregates are Min, Max, Sum, and Avg (average).

Ok, so now let's take a look at this ColdFusion XML stuff in action. To test with, we must build a ColdFusion XML document object:

  • <!--- Build our XML document. --->
  • <cfxml variable="xmlData">
  •  
  • <girls>
  •  
  • <girl id="1">
  • <name>Hayden Panettiere</name>
  • <hair>Blonde</hair>
  • <age>18</age>
  • <build>Athletic</build>
  • <desc>She's 18, I don't have to feel so guilty.</desc>
  • </girl>
  •  
  • <girl id="2">
  • <name>Lindsay Lohan</name>
  • <hair>Blonde</hair>
  • <age>19</age>
  • <build>Normal</build>
  • <desc>Yikes, this girl got problems!</desc>
  • </girl>
  •  
  • <girl id="3">
  • <name>Paris Hilton</name>
  • <hair>Blonde</hair>
  • <age>25</age>
  • <build>Anorexic</build>
  • <desc>I just threw up a bit in my mouth.</desc>
  • </girl>
  •  
  • <girl id="4">
  • <name>Kristen Stewart</name>
  • <hair>Brunette</hair>
  • <age>17</age>
  • <build>Thin</build>
  • <desc>I like her tomboy acting style.</desc>
  • </girl>
  •  
  • </girls>
  •  
  • </cfxml>

Notice that this XML document has a mix of attributes and straight up text node values. Now, we can create an instance of the XmlUtility.cfc ColdFusion component and start testing. To being, let's tackle the problem that you present - getting all the text values. For my demo, I will grab the names of all the girls that are considered "legal":

  • <!--- Create an instance of our XML Utility object. --->
  • <cfset objXmlUtility = CreateObject(
  • "component",
  • "XmlUtility"
  • ).Init()
  • />
  •  
  • <!--- Get an array of the names of all the legal girls. --->
  • <cfset arrValues = objXmlUtility.GetValueArray(
  • xmlData,
  • "//name[ ../age[ text() >= 18 ] ]/text()"
  • ) />
  •  
  • <!--- Dump out the values. --->
  • <cfdump
  • var="#arrValues#"
  • label="Legal girls"
  • />

Here, we are getting all the Name node text values based on those that have sibling Age nodes who's text value greater than or equal to 18. Running the above code, we get the following CFDump output:


 
 
 

 
XML Utility - Getting Array of Text Node Values  
 
 
 

I am getting girl names, but this could just have easily been the EmpID node values that you have in your example.

Ok, now let's take a look at the Aggregate functionality. This is basically a short cut of getting the value array and then running an array method on it yourself. For this example, let's get the maximum age of all the girls in the ColdFusion XML document:

  • <!--- Get the oldest of all the girls' ages. --->
  • <cfset intMax = objXmlUtility.GetValueAggregate(
  • xmlData,
  • "//age",
  • "MAX"
  • ) />
  •  
  • <p>
  • Age Max: #intMax#
  • </p>

Running the above code, we get the following output:

Age Max: 25

Like I said, there is a certain amount of work that needs to be done to perform operations like this in ColdFusion, but at least if you encapsulate it into a user defined function (UDF) or a ColdFusion component like my XmlUtility.cfc, it becomes easier.

Here is the XmlUtility.cfc code:

  • <cfcomponent
  • output="false"
  • hint="Provides some XML utilities that live on top of XmlSearch() and XPath.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Returns an initialized component instance.">
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GetValueAggregate"
  • access="public"
  • returntype="numeric"
  • output="false"
  • hint="Gets the given aggregate of the matched attribute or element text values. Available aggregates are SUM, MIN, MAX, AVG.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="XML"
  • type="any"
  • required="true"
  • hint="The ColdFusion XML document we are searching."
  • />
  •  
  • <cfargument
  • name="XPath"
  • type="string"
  • required="true"
  • hint="The XPAth that will return the XML nodes from which we will be getting the values for our array."
  • />
  •  
  • <cfargument
  • name="Aggregate"
  • type="string"
  • required="true"
  • hint="The SUM, MIN, MAX, AVG aggregate run on the returned values."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Get the value array. --->
  • <cfset LOCAL.Values = THIS.GetValueArray(
  • XML = ARGUMENTS.XML,
  • XPath = ARGUMENTS.XPath,
  • NumericOnly = true
  • ) />
  •  
  • <!--- Check to see which aggregate we are running. --->
  • <cfswitch expression="#ARGUMENTS.Aggregate#">
  • <cfcase value="MIN">
  • <cfreturn ArrayMin( LOCAL.Values ) />
  • </cfcase>
  • <cfcase value="MAX">
  • <cfreturn ArrayMax( LOCAL.Values ) />
  • </cfcase>
  • <cfcase value="AVG">
  • <cfreturn ArrayAvg( LOCAL.Values ) />
  • </cfcase>
  •  
  • <!--- By default, return SUM. --->
  • <cfdefaultcase>
  • <cfreturn ArraySum( LOCAL.Values ) />
  • </cfdefaultcase>
  • </cfswitch>
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GetValueArray"
  • access="public"
  • returntype="array"
  • output="false"
  • hint="Returns an array of of either attribute values or node text values.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="XML"
  • type="any"
  • required="true"
  • hint="The ColdFusion XML document we are searching."
  • />
  •  
  • <cfargument
  • name="XPath"
  • type="string"
  • required="true"
  • hint="The XPAth that will return the XML nodes from which we will be getting the values for our array."
  • />
  •  
  • <cfargument
  • name="NumericOnly"
  • type="boolean"
  • required="false"
  • default="false"
  • hint="Flags whether only numeric values will be selected."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!---
  • Get the matching XML nodes based on the
  • given XPath.
  • --->
  • <cfset LOCAL.Nodes = XmlSearch(
  • ARGUMENTS.XML,
  • ARGUMENTS.XPath
  • ) />
  •  
  •  
  • <!--- Set up an array to hold the returned values. --->
  • <cfset LOCAL.Return = ArrayNew( 1 ) />
  •  
  • <!--- Loop over the matched nodes. --->
  • <cfloop
  • index="LOCAL.NodeIndex"
  • from="1"
  • to="#ArrayLen( LOCAL.Nodes )#"
  • step="1">
  •  
  • <!--- Get a short hand to the current node. --->
  • <cfset LOCAL.Node = LOCAL.Nodes[ LOCAL.NodeIndex ] />
  •  
  • <!---
  • Check to see what kind of value we are getting -
  • different nodes will have different values. When
  • getting the value, we must also check to see if
  • only numeric values are being returned.
  • --->
  • <cfif (
  • StructKeyExists( LOCAL.Node, "XmlText" ) AND
  • (
  • (NOT ARGUMENTS.NumericOnly) OR
  • IsNumeric( LOCAL.Node.XmlText )
  • ))>
  •  
  • <!--- Add the element node text. --->
  • <cfset ArrayAppend(
  • LOCAL.Return,
  • LOCAL.Node.XmlText
  • ) />
  •  
  • <cfelseif (
  • StructKeyExists( LOCAL.Node, "XmlValue" ) AND
  • (
  • (NOT ARGUMENTS.NumericOnly) OR
  • IsNumeric( LOCAL.Node.XmlValue )
  • ))>
  •  
  • <!--- Add the attribute node value. --->
  • <cfset ArrayAppend(
  • LOCAL.Return,
  • LOCAL.Node.XmlValue
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!--- Return value array. --->
  • <cfreturn LOCAL.Return />
  • </cffunction>
  •  
  • </cfcomponent>

I hope this helps you in some way.




Reader Comments

Tim
Aug 27, 2007 at 11:13 AM // reply »
10 Comments

Glad I could inspire you! Ill try it out now.

Tim


Tim
Aug 27, 2007 at 11:31 AM // reply »
10 Comments

Did I miss a link to your XmlUtility.cfc?


Aug 27, 2007 at 11:36 AM // reply »
10,640 Comments

Oh my god! I am such an IDIOT! I forgot to post the code. Give me two seconds :(


Aug 27, 2007 at 11:38 AM // reply »
10,640 Comments

Ok, it's in there now. Really sorry about that... a bit embarrassing :)


Oct 28, 2010 at 10:39 AM // reply »
8 Comments

I just love your code examples.

You kick the ass of livedocs.


Nov 1, 2010 at 9:48 PM // reply »
10,640 Comments

@Phil,

Ha ha, thanks!! That made me smile.


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 12, 2012 at 3:37 AM
Learning ColdFusion 8: CFImage Part III - Watermarks And Transparency
Hi Ben, Just to ask currently it is placed bottom right corner, if i need to replace the same rendered image on the bottom left side or in the bottom center, how that can be calculated. bottom ce ... read »
Feb 11, 2012 at 9:29 PM
Use jQuery's SlideDown() With Fixed-Width Elements To Prevent Jumping
I can't say how glad I am that I found your post. Thank you very much. ... read »
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 »