Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with:

Ask Ben: Getting XML Values And Aggregates In ColdFusion

By Ben Nadel on

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