Ask Ben: Getting Dynamic References To XML Nodes And Setting Text Values In ColdFusion

Posted July 8, 2008 at 9:24 AM by Ben Nadel

Tags: ColdFusion, Ask Ben

Simon Horwith and I have some very different and very strong views on dynamic variable naming and setting conventions. He thinks one thing, I think another; but, at the end of the day, both of us get the job done with our own methodologies. In a conversation that we were having about ColdFusion's archaic SetVariable() method, Horwith stated that one of the places he uses it is to set XmlText values on user-selected, deeply nested XML nodes. He presented me with a challenge to see if I could come up with a way to do this without using SetVariable(). Here are the rules Horwith laid forth:

First: Create a fairly complex XML file - an XML file with different types of nodes nested within each other that each use attributes and/or XMLText - and make sure it has at least 3 or 4 levels of nesting.

Second: The challenge: create an html/CF interface (no AJAX or Flex) that let's you pick a top level node, then submit, then pick a node of the parent node that was submitted, and submit... etc., etc. - in the real world it should let the end user drill as deep as they need/want to - at any given level you should be able to type into a text-area and set the XML Text for the current tag. What's tricky about this is that the only way to deal with it is to pass a string from form to form that contains the string-path to where you're working. You might be thinking that you can just set a session scope variable on each form submission... but the problem is that if the XML is complex enough that won't work, as only XML Nodes and XML Node XML Attributes will create pointers. Go ahead... give it a try if you have the time :)

Really, it doesn't matter how deeply nested the XML nodes are or how complicated the XML tree is. If you can make this dynamic for one parent-child step, then it's pretty much the same thing for N-steps deep with whatever breadth you may choose. As such, here is the XML tree that I created and cached in my APPLICATION scope:

  • <root>
  • <a1>
  • <b1>
  • <c>C Text Node</c>
  • </b1>
  • <b2>
  • <c>C Text Node</c>
  • </b2>
  • </a1>
  • <a2>
  • <b1>
  • <c1>
  • <d1>
  • <e>E Text Node</e>
  • </d1>
  • </c1>
  • </b1>
  • <b2>
  • <c1>C Text Node</c1>
  • </b2>
  • </a2>
  • <a3>
  • <b1>B Text Node</b1>
  • </a3>
  • </root>

As you can see, I am keeping it fairly simple with my node naming. Some of them have an XML Text value, others do not (or rather, it is just white space). Before we get into the code, let's take a quick look at a demo video so you can get a better overview of how this works:


 
 
 

 
 
 
 
 

From the video, it is easy to tell that the text value I am setting is actually getting stored in the cached XML tree object, which is cached in our APPLICATION scope.

When I first started to think about this problem, I was thinking in the SetVariable() mindset, since that is how Simon Horwith's solution works. Aside from the fact that I don't use SetVariable(), the concept of a dynamic variable references in XML made me very uncomfortable. I didn't know why at first and then it dawned on me - I never handle XML that way; I have a very specific way of handling all of my XML problems and that is to first get a short-hand reference (pointer) to my XML node of choice and then use that reference to do anything that I wanted in terms of accessing or mutating information.

Once I had that mentality in place, it was just a matter of figuring out a way to keep track of my location in the XML document object. If you remember that XML nodes and node properties basically act like either structs or arrays, you might realize that keeping track of your current node can be done easily by simply keeping track of your ChildNodes array index values. Think about it this way, you might have an XML path that looks like this:

xml.XmlRoot.XmlChildren[ 2 ].XmlChildren[ 1 ].XmlChildren[ 5 ]

When you see the path this way, you can clearly see that the "directions" are the indexes of the XmlChildren arrays. The tricky part is that the XmlRoot object has an implicit "1" index. So really, the above path can be defined using the following comma-delimited list on index values:

1,2,1,5

Ok, but now that we have an easy way to store the path, how do we go about turning that path into an XML node pointer? It turns out, this is actually pretty easy - the sexy marriage of XPath and XmlSearch(). Really, what we want to do is turn the above, comma-delimited list into the following XPath:

/*[ 1 ]/*[ 2 ]/*[ 1 ]/*[ 5 ]

While this might seem like a complicated jump, it is merely a line of string concatenation and replacement. And, once we have our XPath in place, we can easily get a reference to the target node. And, once we have a pointer to the target node, we can easily get and set values of that node, such as the XmlText value laid out in the above challenge.

Now that you see where I am going with this, let's take a look at the code behind this demo:

  • <!---
  • Param XML path. This is a comma delimited list of child
  • array indexes. If it is blank then no path has been
  • yet to be selected.
  • --->
  • <cfparam
  • name="URL.path"
  • type="regex"
  • pattern="^\d+(,\d+)*$"
  • default="1"
  • />
  •  
  •  
  • <!---
  • Check to see if we need to create or reset the XML data
  • element cached in our Application.
  • --->
  • <cfif (
  • (NOT StructKeyExists( APPLICATION, "Data" )) OR
  • StructKeyExists( URL, "reset" )
  • )>
  •  
  • <!--- Create a nested XML example. --->
  • <cfxml variable="APPLICATION.Data">
  •  
  • <root>
  • <a1>
  • <b1>
  • <c>C Text Node</c>
  • </b1>
  • <b2>
  • <c>C Text Node</c>
  • </b2>
  • </a1>
  • <a2>
  • <b1>
  • <c1>
  • <d1>
  • <e>E Text Node</e>
  • </d1>
  • </c1>
  • </b1>
  • <b2>
  • <c1>C Text Node</c1>
  • </b2>
  • </a2>
  • <a3>
  • <b1>B Text Node</b1>
  • </a3>
  • </root>
  •  
  • </cfxml>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Convert the comma delmitted list of child array
  • indexes into an XPath string so that we can get
  • the selected node.
  •  
  • Example:
  • 1,3,2
  •  
  • Becomes:
  • /*[1]/*[3]/*[2]
  • --->
  • <cfset strXPath = (
  • "/*[" &
  • Replace(
  • URL.path,
  • ",",
  • "]/*[",
  • "all"
  • ) &
  • "]"
  • ) />
  •  
  •  
  • <!--- Get the selected noded based on the XPath. --->
  • <cfset arrNodes = XmlSearch(
  • APPLICATION.Data,
  • strXPath
  • ) />
  •  
  • <!---
  • Since XmlSearch() returns an array, let's get a pointer
  • directly the XML node in question. If our search went
  • bad, this will throw an error.
  • --->
  • <cfset xmlNode = arrNodes[ 1 ] />
  •  
  •  
  • <!---
  • Check to see if we have a URL field for the text. If
  • so, then let's store that text into the node. Since this
  • node is passed by reference, this update will automatically
  • propogate to the cached APPLICATION scope data.
  • --->
  • <cfif StructKeyExists( URL, "xml_text" )>
  •  
  • <!--- Update xml text (automatically cached). --->
  • <cfset xmlNode.XmlText = XmlFormat( URL.xml_text ) />
  •  
  • </cfif>
  •  
  •  
  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Dynamic XML Reference And Update Demo</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Dynamic XML Reference And Update Demo
  • </h1>
  •  
  • <p>
  • <!---
  • Check to see if we have more than one child node
  • indexes. Since we are starting in the root node,
  • then we need more than one to have a parent.
  • --->
  • <cfif (ListLen( URL.path ) GT 1)>
  •  
  • <a href="#CGI.script_name#?path=#ListDeleteAt( URL.path, ListLen( URL.path ) )#">Parent Node</a>
  •  
  • <cfelse>
  •  
  • <em>No Parent</em>
  •  
  • </cfif>
  • </p>
  •  
  • <p>
  • Current Node:
  • <strong>#xmlNode.XmlName#</strong>
  • </p>
  •  
  •  
  • <ol>
  •  
  • <!---
  • Loop over the child nodes of the current node.
  • This will allow us to drill down into the
  • children of the currently selected node.
  • --->
  • <cfloop
  • index="intIndex"
  • from="1"
  • to="#ArrayLen( xmlNode.XmlChildren )#"
  • step="1">
  •  
  • <!---
  • For each child node, we need to add the
  • current child array index to ongoing XML
  • path.
  • --->
  • <li>
  • <a href="#CGI.script_name#?path=#ListAppend( URL.path, intIndex )#">#xmlNode.XmlChildren[ intIndex ].XmlName#</a>
  • </li>
  •  
  • </cfloop>
  •  
  • </ol>
  •  
  •  
  • <form
  • action="#CGI.script_name#"
  • method="get">
  •  
  • <!--- Pass back path to this node. --->
  • <input
  • type="hidden"
  • name="path"
  • value="#URL.path#"
  • />
  •  
  • <fieldset>
  • <legend>Current Node Data</legend>
  •  
  • <p>
  • Node: #xmlNode.XmlName#
  • </p>
  •  
  • <p>
  • Text:
  •  
  • <input
  • type="text"
  • name="xml_text"
  • value="#xmlNode.XmlText#"
  • size="40"
  • />
  •  
  • <input type="submit" value="Update" />
  • </p>
  •  
  • </fieldset>
  •  
  • </form>
  •  
  •  
  • <br />
  •  
  • <!---
  • Output XML data for debugging. THis is so we can
  • see how our data is actually being cached.
  • --->
  • <cfdump
  • var="#APPLICATION.Data#"
  • />
  •  
  • </body>
  • </html>
  •  
  •  
  • </cfoutput>

So, while Simon Horwith and I have very different views on dynamic variable naming / setting, in the end, it turned out the best solution for this problem had nothing at all to do with dynamic variable naming / setting. I can't think of anything that's easier than passing around a comma delimited list of child node indexes.



Reader Comments

Jul 8, 2008 at 10:37 AM // reply »
4 Comments

Cool Dude!


Jul 8, 2008 at 3:21 PM // reply »
110 Comments

Nice! +1 for Jing for your videos as they are not blocked at my work (the other videos were) :)


Jul 8, 2008 at 5:01 PM // reply »
11,243 Comments

Glad you guys are liking the videos. I am really getting into them. I think it goes a long way to really demonstrate what the code is doing in a way that just looking at the code cannot.

This morning, I figured out to configure it to FTP the videos to my site rather than using the central server (which has a content size limit).


Jul 9, 2008 at 7:04 AM // reply »
148 Comments

Thanks for the video! It helps me to get a more clear picture of what you guys (and gals) are all talking about. I'm a visual learner and sometimes actually seeing the code working in front of me gives me a more clear picture.


Jul 9, 2008 at 8:17 AM // reply »
11,243 Comments

@Lola,

Glad to know that it is working. I will continue to make those when I feel they add value.


Nov 15, 2012 at 7:32 AM // reply »
2 Comments

This article was fantastically helpful. I have an XML structure with complex sub-nodes and needed to put values into the nodes and was having difficulty addressing them correctly.

Although I did not actually end up using the code in this example, studying it helped me realise what I was doing wrong and fixed my problem.

Thanks for all your great CF know-how.


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 23, 2013 at 5:19 AM
Ask Ben: Print Part Of A Web Page With jQuery
How to print also the background color of table cells and table lines ... read »
May 23, 2013 at 3:55 AM
Javascript Array Methods: Unshift(), Shift(), Push(), And Pop()
very interesting and helpful too. ... read »
May 22, 2013 at 5:35 PM
Script Tags, jQuery, And Html(), Text() And Contents()
This is still an issue 2 years later. jQuery is supposed to remediate these cross browser issues, no? I have been unable to find any statement from the jQuery team calling this behavior "by de ... read »
May 22, 2013 at 12:44 PM
Ask Ben: Query Loop Inside CFScript Tags
In cf10, if you call a function that has: local.result = {}; local.result.msg = ""; local.svc = new query(); local.svc.setSQL("SELECT * FROM..."); local.obj = local.svc.exe ... read »
May 22, 2013 at 12:29 PM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben: What version of Java are you using? Also, did you test users.id to see what Java reports as the data type? I wonder if it's not a Java primitive data type, but getting returned as something ... read »
May 22, 2013 at 11:47 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dana, Awesome - so it looks like this bug was fixed in ColdFusion 10. Thanks so much for double-checking that. ... read »
May 22, 2013 at 11:37 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
When I c&p and run on cf10, I get: Selected User IDs: 1,4 User 1 selected: YES - YES User 2 selected: NO - NO User 3 selected: NO - NO User 4 selected: YES - YES User 5 selected: NO - ... read »
May 22, 2013 at 11:27 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Tom, Good thought, but no dice. Both of these still exhibit the same behavior: users.id[ users.currentRow ] users[ "id" ][ users.currentRow ] It's just something whacky happening with ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools