Ask Ben: Iterating Over A ColdFusion XML Document
Posted November 15, 2007 at 7:35 AM by Ben Nadel
I would appreciate your help, in providing a workable nested solution for the following type of XML structure.
Where the Info Main is the top structure containing collections of images and names. I have no problem in looping the Info Main top structure as per:
<cfloop index="i" from="1" to="#arrayLen(Info_Main_Array)#"> etc, but I can't seem to work out how to connect the sub-lists of images and names. Your help would be much appreciated.
For starters, let's just make sure that we're all on the same page as to what constitutes a valid XML document in ColdFusion (or anywhere). In a valid XML document, there has to be some sort of root XML node that contains all the other nodes. Also, I don't believe the XML node names can have spaces in them. So, the node "Info Main" is not a valid name. I don't think the parser would be able to tell where the name ends and a poorly formed attribute begins. So, let's take your XML document and convert into something that the ColdFusion XML parser can actually work with:
- <!--- Create the XML document object. --->
- <cfxml variable="xmlDoc">
- <info-main id="im1">
- <image-list id="il1">
- <image id="i1" />
- <image id="i2" />
- <name-list id="nl1">
- <name id="n1" />
- <name id="n2" />
- <name id="n3" />
- <name id="n4" />
- <info-main id="im2">
- <image-list id="il2">
- <image id="i3" />
- <image id="i4" />
- <name-list id="nl2">
- <name id="n5" />
- <name id="n6" />
- <name id="n7" />
- <name id="n8" />
As you can see, I have wrapped your XML nodes into a single root node, info. I have also lower cased all the names (remember XML is very much case sensitive) and I have added hyphens in the node names that had spaces. I have also added ID attributes to each node so that as we traverse the ColdFusion XML document object model, we will easily be able to output the ID to track our progress.
When extracting data from a ColdFusion XML document object, we can get really slick and do some cool XmlSearch() stuff with XPath, but, for this, let's keep it to some simple direct XML node access. To do this, you simply have to understand how the ColdFusion XML document is structured. Each node has a set of attributes (XmlAttributes) and a set of direct child nodes (XmlChildren). The XmlAttributes object is a key-value struct. The XmlChildren is an array.
You can get to each node using these two sets mentioned above, or you can also treat the XML document as a pseudo struct/array document. For example, if we had a parent node containing a child node and we wanted to access the second child of the first parent, we could do this:
xmlDoc.data.XmlChildren[ 1 ].XmlChildren[ 2 ]
... or, we could treat it more like a struct / array beast:
xmlDoc.data.parent[ 1 ].child[ 2 ]
Both would give you the same node reference.
That's not the best example since you can't see the real XML strcuture, so let's get back to your example. Using a combination of the two access methods, we can not iterate over the known structure of our test ColdFusion XML document:
- <!--- Loop over the info-main nodes. --->
- to="#ArrayLen( xmlDoc.info.XmlChildren )#"
- <!--- Get a short hand for the current info-main node. --->
- <cfset xmlInfoMain = xmlDoc.info[ "info-main" ][ intMainIndex ] />
- info-main[ id = #xmlInfoMain.XmlAttributes.id# ]<br />
- <!--- Get the image-list children. --->
- <cfset arrImageList = xmlInfoMain[ "image-list" ] />
- <!--- Loop over the image list. --->
- to="#ArrayLen( arrImageList )#"
- <!--- Get a short hand for the current image-list node. --->
- <cfset xmlImaegList = arrImageList[ intImageListIndex ] />
- . . image-list[ id = #xmlImaegList.XmlAttributes.id# ]<br />
- <!--- Loop over image nodes. --->
- to="#ArrayLen( xmlImaegList.image )#"
- . . . . image[ id = #xmlImaegList.image[ intImageIndex ].XmlAttributes.id# ]<br />
- <!--- Get the name-list children. --->
- <cfset arrNameList = xmlInfoMain[ "name-list" ] />
- <!--- Loop over the name list. --->
- to="#ArrayLen( arrNameList )#"
- <!--- Get a short hand for the current name-list node. --->
- <cfset xmlNameList = arrNameList[ intNameListIndex ] />
- . . name-list[ id = #xmlNameList.XmlAttributes.id# ]<br />
- <!--- Loop over name nodes. --->
- to="#ArrayLen( xmlNameList.name )#"
- . . . . name[ id = #xmlNameList.name[ intNameIndex ].XmlAttributes.id# ]<br />
- <br />
Running the above code, we get the following output:
info-main[ id = im1 ]
. . image-list[ id = il1 ]
. . . . image[ id = i1 ]
. . . . image[ id = i2 ]
. . name-list[ id = nl1 ]
. . . . name[ id = n1 ]
. . . . name[ id = n2 ]
. . . . name[ id = n3 ]
. . . . name[ id = n4 ]
info-main[ id = im2 ]
. . image-list[ id = il2 ]
. . . . image[ id = i3 ]
. . . . image[ id = i4 ]
. . name-list[ id = nl2 ]
. . . . name[ id = n5 ]
. . . . name[ id = n6 ]
. . . . name[ id = n7 ]
. . . . name[ id = n8 ]
I hope this example helps you out in some way.
What Other People Are Searching For
Can you help me Iterate over this advanced XML object? http://www.nycwebdesign.net/xmlFEED.cfm
That is the link i'm getting...
I have this so far:
XMLContent = trim(objGet.FileContent);
XMLContent = XMLParse(XMLContent);
I basically need to output this XML and set the following vars:
Your link is giving me a ColdFusion error.
Hi Ben...try it again. Thanks. I am curious to see how you go about iterating through that XML feed.
Right now, there data on that page is a CFDump. I would need a page that points to the XML data itself in order to be able to parse and traverse it.
Thanks, Ben! This really helped me debug a report I was working on earlier today. I was asked to run a deep text search on all the files on our site in search of some outdated URLs. Then I was to save that report to Excel. My first thought was to run the search with regular expressions and Dreamweaver. The search went through fine. It was getting to save the report in a user-friendly format where I ran into problems.
The only option for saving the search results was a Dreamweaver-specific XML format. (Not even a tab-delimited format? Come on!) That left with me little choice but to figure out how to turn this verbose XML document into something more usable. Enter CF support of XML and my being about six years behind in this area.
I looked through the guides to see how it was supposed to be done. My first attempt seemed rather logical. Using CFFile, I would load the contents of the Dreamweaver XML file to a variable, and then use CFXml to create an XML document object.
No matter what I tried, I kept getting this annoying "Content not allowed in prolog" error. I found a couple of posts on this topic including one on this site. I made sure there were no spaces before the opening XML tag. I even used regular expressions to try and eliminate any potential Byte-Order-Markers (BOMs) at the beginning. Nothing worked.
I'll tell you what did work. The sample XML here. It got parsed with no problems whatsoever. Seeing how I was able to get the CF code working using the sample in this blog post, I got desperate and decided to copy-and-paste the contents of the XML file into my CFXml block. Well what do you know? That worked too!?!??
For some reason I might never know, I could never get CFXml to create the XML object if I read in the XML code using CFFile. If the XML was hard-coded into the page, however, all was dandy. So I was able to create the report, albeit several hours later. But now I know, and knowing is half the yadda yadda yadda.
On a side note: I learned just a few minutes ago that Eclipse's search results are copy-and-paste-able. Maybe next time I'll go with Eclipse :-)
Thanks for this helpful and clear explanation. Exactly what I was looking for.
Awesome - glad this helped. ColdFusion's XML documents are pretty awesome.
How would you parse over this?
<transactionNumber>TEST TRANSACTION NUMBER</transactionNumber>
<pointCategoryResult Title="Placeholder" PointCategoryID="174B7C2A-2007-0520-2718-5F6186597734" Percentage="98" MinBound="0" MaxBound="50" ScoreValue="49"/>
I can get/pull out everything until I get to the <pointCategoryResults> node. I need to be able to read the <pointCategoryResult> elements of Title and Percentage.
I think i'm having a similar problem to @Rob
I think the problem is being caused because of namespaces.
I have put my content on http://www.fusebox.co.za/terminal_query.cfm where you will see the raw XML as well as the dump.
The XML is returning 2 transactions which i need to get into a digestable format.
Would you mind taking a look?
I'm sure im not the only one going grey because of this.
Thank you so much for sharing your knowledge!
Just thought I'd share the solution to my question above:
<cfset xmlp = xmlparse(cfhttp.filecontent)>
<cfset xmlitems = xmlp['SOAP-ENV:Envelope']['SOAP-ENV:Body']['ns1:queryResponse']['returnTranData']>
<cfset availableBalance = xmlp['SOAP-ENV:Envelope']['SOAP-ENV:Body']['ns1:queryResponse']['availableBalance']>
<cfoutput>Available balance: #availableBalance#</cfoutput><Br /><Br />
<cfloop from="1" to="#arraylen(xmlitems['item'])#" index="i">
TransactionID: #xmlitems['item'][i].TransactionID.xmltext#<br />
TranType: #xmlitems['item'][i].TranType.xmltext#<br />
For those who need it: Why don't you use recursive function to loop & print parents-children :
- <cffunction name="outputNode">
- <cfargument name="parentnode">
- <cfargument name="spacing" required="no">
- <cfif isDefined('arguments.spacing')>
- <cfset var spaces = arguments.spacing >
- <cfset var spaces = 0 >
- <cfset var returnval = "">
- <cfset var returnval = "#returnval##RepeatString(' ',spaces)##Replace(parentnode.XmlName,'_',' ','ALL')#">
- <cfif Len(parentnode.XmlText) gt 0>
- <cfset var returnval = "#returnval#: #parentnode.XmlText#">
- <cfset var returnval = "#returnval#<br/>">
- <cfset var spaces = spaces + 2 >
- <cfloop index="i" from="1" to="#ArrayLen(parentnode.XmlChildren)#">
- <cfset var returnval= "#returnval##outputNode(parentnode.XmlChildren[i],spaces+2)#">
- <cfreturn returnval>
to my previous post. Call it like this: