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 CFinNC 2009 (Raleigh, North Carolina) with: Mike Brunt

Ask Ben: Selecting XML Attributes Given Other XML Attributes

By Ben Nadel on

Earlier today, someone asked me about searching XML documents in such a way that he only wanted to select a tag attribute if the parent tag had another attribute set to a given value. This is actually a simple task with the use of a Predicate. As a review from my ColdFusion XPath and XmlSearch() presentation, predicates are XPath constructs contained in square brackets that filter the results of the returned nodes. Predicates need to result in a boolean-true in order for the selected node to be returned in the final node set.

In our case, there are two ways we can look at the problem that have slightly different XPath values. We want to:

  • Select an attribute node that is part of a element node that has an attribute of a given value.
  • Select an attribute node that has a sibling attribute node of a given value.

While these might sound like the same thing, and do, in fact, result in the same node set, they require different XPath values. Both of these situations are demonstrated in this example:

  • <!--- Define our ColdFusion XML document object. --->
  • <cfxml variable="xmlGirls">
  •  
  • <girls>
  • <girl
  • name="Samantha"
  • age="27"
  • hair="Blonde"
  • />
  • <girl
  • name="Kim"
  • age="32"
  • hair="Brunette"
  • />
  • <girl
  • name="Cindi"
  • age="25"
  • hair="Black"
  • />
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!---
  • Get the Name attribute nodes of all the girls
  • who are brunetted. We are going to be doing this
  • by only looking in girl nodes that have a hair
  • attribute that is brunette.
  • --->
  • <cfset arrNodes1 = XmlSearch(
  • xmlGirls,
  • "//girl[ @hair = 'Brunette' ]/@name"
  • ) />
  •  
  • <!---
  • Get the Name attribute nodes of all the girls
  • who are brunetted. We are going to be doing this
  • by getting all name nodes who have a sibling
  • attribute node, hair, that is Brunette.
  • --->
  • <cfset arrNodes2 = XmlSearch(
  • xmlGirls,
  • "//girl/@name[ ../@hair = 'Brunette' ]"
  • ) />
  •  
  •  
  • <!--- Output the matching nodes. --->
  • <cfdump
  • var="#arrNodes1#"
  • label="Names of Burnette Girls - Method ##1"
  • />
  •  
  • <!--- Output the matching nodes. --->
  • <cfdump
  • var="#arrNodes2#"
  • label="Names of Burnette Girls - Method ##2"
  • />

Notice that in our first ColdFusion XmlSearch() call, our XPath value is first limiting on the Girl node, using a predicate that requires the Hair attribute to be Brunette. Then in our second ColdFusion XmlSearch() call, our predicate is requiring a sibling Hair attribute with no explicit mention of the parent tag (other than by relative relationship).

Running the above tag, we get the two CFDump outputs:


 
 
 

 
ColdFusion XmlSearch() That Uses XPath  
 
 
 

 
 
 

 
ColdFusion XmlSearch() That Uses XPath  
 
 
 

As you can see, both return the proper Name attribute, Kim. Now, is there a difference between the two different XPath values used? From a readability standpoint, I think the first one is better. From a performance standpoint, I am going to assume that the first one is also a better choice. Just as with a SQL WHERE clause, I think in an XPath statement, you are gonna get better performance by putting the most limiting statements first; filtering on the Girl node will result in less Name attribute node evaluations and therefore might perform better.

Hope that helps a bit.




Reader Comments

Ben,

I was looking at doing this exact same thing. How would it work if I wanted to test/search on two variables say Brunette and age = 32

Reply to this Comment

@Alan,

XPath supports some AND/OR logic in the predicates:

//girl[ @hair = 'Brunette' and @age = '32' ]/@name

Notice that the "and" is lowercase; this is required. An uppercase AND will not work properly.

Reply to this Comment

Thanks for this example. It was exactly what I was looking for. Sometimes it's impossible to use XPath without a resources of many solid examples.

Reply to this Comment

@John,

Awesome! Glad to help. If you run into any more issues with XPath, please drop me a line and I'd be happy to whip up a good demo or tutorial.

Reply to this Comment

Saved me AGAIN! 2 in one day...
it's getting to the point that I go to bennadel.com first, then the cfdocs.
(in most cases google finds Ben's stuff first anyway!)

Reply to this Comment

Seems like I'm missing something. If I take this code and put it in a .cfm file and try to run it I get this error:
Detail Premature end of file.
ErrNumber 0
ExceptionMessage Premature end of file.
Message An error occured while Parsing an XML document.

Reply to this Comment

@Tom,

That's odd. Looks like a malformed XML problem. Make sure you aren't missing any of the ">" closing brackets or something.

Reply to this Comment

Thanks for the response. I finally discovered that I was getting this error because I had cfsetting enablecfoutputonly="yes" in Application.cfc, and was neither setting it to false elsewhere nor bracketing cfxml with cfoutput.

Reply to this Comment

This is truly amazing stuff for me Ben. I knew there was a way to build these types of specific-data arrays without having to loop over multiple nodes, etc.
<br /><br />
I am having a bit of difficulty accessing the XmlValue of the returned attributes, however. In other words, if I wanted an array of just the value of the name attribute for each girl, I would guess aGirlNames=XmlSearch(xmlGirls,"/girls/girl/@name");
<br /><br />
My problem is that I end up with an array where each of the three elements is another array. How do I get at each of the names in XmlValue?
<br /><br />
Thanks again for sharing all of the solid development tips over the years!

Reply to this Comment

Shawn --- you can get it by doing this after you've gotten the array:

<cfset aGirlName = #aGirlName[1][1]#>

BTW -- I have found that you may also be getting the header of the XML too along with the value (<?xml version=1.0 encoding=UTF-8?). So numbers won't necessrily work properly unless you strip out the extra text.

You can check on that as needed once you've gotten your value out.

Reply to this Comment

Hi Alan,
Thanks for replying, that gets me a bit closer to what I am looking to do. After re-reading my question, I realize I probably wasn't real clear on my need.

<cfset aGirlName = #aGirlName[1][1]#> will only give me name/value pairs of XmlName, XmlType, and XmlValue, and for only one girl. Even if I dump out #aGirlName[1][1].XmlValue#, I will still only be dealing with one girl.

I was hoping to use an XPath expression to return an array with just the values of #aGirlName[i][j].XmlValue#.

I am looking to avoid looping over the resulting array, since it effectively cancels out everything I gain by using the XPath expression in the first place. I was using a nested loop to do this before, and was hoping I could replace my original process with an XPath expression.

I hope that makes more sense.

BTW: Sorry for the <br /> tags in my previous post.

Reply to this Comment

@Shawn,

I know exactly what you are trying to do. The problem is that in XML, "attributes" are really just different types of "nodes" (as opposed to element nodes based on tag names); in other words, they aren't just simple text values - they are node structures in which the property "xmlValue" is the textual attribute value.

If you want to *only* get back the value, you'd have to wrap this access in some sort of UDF (user defined function) that maps the node array to a simple value array.

Reply to this Comment

Hey Ben,

Could you give us an example of how to return nodes according to a date range?

I tried posting sample XML within <code></code> tags, but got an error:

Your comment contains restricted HTML elements (A).

For a live-scoring app, I need to get the first node where the MatchDate attribute "startDateLocal" is on or after the current date.

I'm struggling with namespace errors using such code as:

  • <cfset current matchNodes = xmlSearch(scheduleXml,"/SportData/Sport/Schedules/Match/MatchDate[(@StartDateLocal) >= '#now()#']")>

and I've concluded that I simply don't know what I'm doing.

Any help would be hugely appreciated.

Reply to this Comment

Correction to that code provided:

  • <cfset matchNodes = xmlSearch(scheduleXml,"/SportData/Sport/Schedules/Match/MatchDate[(@StartDateLocal) >= '#now()#']")>

Reply to this Comment

@Paul,

I am not sure that the version of XPath in ColdFusion knows how to support date/time stamps. What I might recommend, without having tested it, is to store the dates in numeric format in your XML. To get this, simply multiple the date by 1:

<tag datetime="#dateOfMatch * 1#">

... This will give you something like:

<tag datetime="12342.43232">

... Then, you can probably use XPath to do a numeric comparison of the "Date time" stamp using numbers.

Of course, assuming you're getting your XML from somewhere else, this probably isn't an option (and might not even work anyway).

You might just have to fall back on doing this manually - ie. getting all the nodes and then filtering the node array once you get it back in ColdFusion.

Reply to this Comment

Hi Ben,

Do you know if I can use concat function in XmlSearch?
here's sample xml
...
<iattrval attr="5" id="40" value="First" />
<iattrval attr="10" id="110" value="Blue" />
<iattrval attr="11" id="116" value="25" />
...

here's what I am trying
//iattrval[@attr=11]/concat(@id,'_',@value)

to get following result
...
116_25
...

Now, if I execute this in stylus studio xpath query editor I do get the desired result but it fails in ColdFusion XmlSearch function with following error message

An error occured while Searching an XML document.
Unknown nodetype: concat

Any help on this is greatly appreciated as always.

Reply to this Comment

The example was awesome. Struggled for almost a day to get the similar stuff done, was about to give up then by luck got a reference to this page.

Thanks for sharing

Reply to this Comment

Thanks for clear explanation! This was very useful when I tried to handle XMLs in a Jelly script.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.