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 »
11,246 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 »
11,246 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 »
11,246 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
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 25, 2013 at 10:08 AM
Using "//" And ".//" Expressions In XPath XML Search Directives In ColdFusion
@Ben, my question is that i want the current node with its tag and its parent node. i just want only that data. So, give me the solution for that. and remember solution is working on " xpath 1.0 ... read »
May 25, 2013 at 10:01 AM
Using "//" And ".//" Expressions In XPath XML Search Directives In ColdFusion
hey ben, i want get my current node tag and also want the root node tag withing. So, how can i fix it.. ! ... read »
May 24, 2013 at 5:39 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
@Adam Oops! My mistake! I hadn't gotten that far in my testing - I'm still baby stepping my way through the process. ... read »
May 24, 2013 at 5:13 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
Hi Jason, Thanks for checking up on that, but I still stand firm on my position. :) There are actually two listLast()'s in use, and you're right that the one using a space as a delimiter is fine. ... read »
May 24, 2013 at 4:45 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
@Ben I have been lurking your site for quite some time, and haven't stepped up to comment until today. Thanks for all the great info - keep it up! @Adam I believe you are mistaken... as the commen ... read »
May 24, 2013 at 11:21 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@WebManWalking, Ha ha, let's us never speak of justifying "##" notation again :P ... read »
May 24, 2013 at 11:18 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, Ah, so it was indeed how I vaguely remembered it to be: A direct assignment value = users.id[ i ] causes value to retain the sticky datatype of the query column. Although unnecessary in ... read »
May 24, 2013 at 9:11 AM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Brandon, Hi, No, I haven't been able to do that. I have just kept it as it is. ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools