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 the jQuery Conference 2010 (Boston, MA) with:

Getting A ColdFusion XML Document Reference From An XML Node In Order To Create A New XML Element

By Ben Nadel on
Tags: ColdFusion

This weekend, I was experimenting with some sexy ColdFusion XML manipulation when I ran into a bit of a problem. I found myself in a situation in which I needed to create a new XML element based on an existing XML element. The problem with doing this is that in order to create a new XML element in ColdFusion, you need a reference to the XML document in which the new element will be created (XML elements are "owned" by an XML document). So, if you only have an existing XML element, you need to finagle your way back up to the original XML document reference.

In ColdFusion, XML can be a bit confusing because there is a subtle yet very important difference between a ColdFusion XML document and the XML data that it contains. This difference is made even less obvious when you consider that xmlSearch() can be used to search both XML documents and XML nodes. Most of the time, this difference doesn't matter to us; but, when we need to create a new XML element, understanding this difference is critical. To see why, take a look at the following code:

  • <!--- Create a simple XML document. --->
  • <cfxml variable="girls">
  •  
  • <girls>
  • <girl>
  • <name>Katie</name>
  • </girl>
  • <girl>
  • <name>Kim</name>
  • </girl>
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!---
  • Create a new element for the girls document. When doing
  • this, we are passing in the XML document reference.
  • --->
  • <cfset isHotNode = xmlElemNew( girls, "isHot" ) />
  •  
  • <!---
  • Create another new element for the girls document; however,
  • this time, we are going to use an XML node as the "parent"
  • document.
  • --->
  • <cfset isCuteNode = xmlElemNew( girls.xmlRoot, "isCute" ) />
  •  
  •  
  • <!---
  • Now that we have tried to create our two nodes, let's check
  • to make sure that both of them exist as variables.
  • --->
  • <cfoutput>
  •  
  • isHotNode Exists: #structKeyExists( variables, "isHotNode" )#
  •  
  • <br />
  • <br />
  •  
  • isCuteNode Exists: #structKeyExists( variables, "isCuteNode" )#
  •  
  • </cfoutput>

In this demo, we have a ColdFusion XML document referenced in the variable, girls. When then try to create two new XML elements for said XML document using xmlElemNew(). One call to xmlElemNew() uses the girls variable while the other uses the root XML element. When we run this code, we get the following output:

isHotNode Exists: YES

isCuteNode Exists: NO

As you can see, when using the XML document reference, xmlElemNew() was able to create and return a new XML element. When using an XML element, on the other hand, xmlElemNew() returned null. In other words, ColdFusion could not create a new XML element based on another XML element.

I think this is a significant shortcoming of the xmlElemNew() function; but, we can get around it. Look at how we got the root XML element in the above demo:

  • girls.xmlRoot

The XML element that we used in the latter part of our demo - the root XML element - is a child of the XML document. This then begs the question - is the ColdFusion XML document a parent of the root XML element? To test this, we can rerun the above code using an xmlParent reference:

  • <!--- Create a simple XML document. --->
  • <cfxml variable="girls">
  •  
  • <girls>
  • <girl>
  • <name>Katie</name>
  • </girl>
  • <girl>
  • <name>Kim</name>
  • </girl>
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!---
  • Create a new element for the girls document. When doing
  • this, we are passing in the XML document reference.
  • --->
  • <cfset isHotNode = xmlElemNew( girls, "isHot" ) />
  •  
  • <!---
  • Create another new element for the girls document; however,
  • this time, we are going to dip down into the actual XML nodes
  • and then come back up to the document level using xmlParent.
  • --->
  • <cfset isCuteNode = xmlElemNew( girls.xmlRoot.xmlParent, "isCute" ) />
  •  
  •  
  • <!---
  • Now that we have tried to create our two nodes, let's check
  • to make sure that both of them exist as variables.
  • --->
  • <cfoutput>
  •  
  • isHotNode Exists: #structKeyExists( variables, "isHotNode" )#
  •  
  • <br />
  • <br />
  •  
  • isCuteNode Exists: #structKeyExists( variables, "isCuteNode" )#
  •  
  • </cfoutput>

This time, when creating the isCuteNode XML element, we dip down into the root XML element but then try to climb our way back up to the XML document using xmlParent:

  • girls.xmlRoot.xmlParent

When we run this code, we get the following output:

isHotNode Exists: YES

isCuteNode Exists: YES

Woohoo! ColdFusion was able to create both XML elements. This tells us that the XML document reference required by the xmlElemNew() function can be obtained from the root XML element.

In the preceding example, it was easy to get the XML document reference because we started with the root XML node; but, what if we don't have the root XML node? What if we have some deeply nested XML element? This was exactly the situation I found myself in over the weekend - I had to create a new XML element based on some arbitrary existing XML node reference.

In that kind of a situation, a single call to xmlParent probably won't do the trick (unless you have the root XML element). As such, we need a way to get to the root XLM element such that we can get to the parent XML document. Luckily, xmlSearch() allows us to do just that. By using the XPath query, "/*", we can select the root XML element from any XML node starting point:

  • <!--- Create a simple XML document. --->
  • <cfxml variable="girls">
  •  
  • <girls>
  • <girl>
  • <name>Katie</name>
  • </girl>
  • <girl>
  • <name>Kim</name>
  • </girl>
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!--- Get the katie node. --->
  • <cfset katieNodes = xmlSearch( girls, "//name[ text() = 'Katie' ]" ) />
  •  
  • <!---
  • xmlSearch() always returns an array of nodes. Get the first
  • (only) resultant node.
  • --->
  • <cfset katie = katieNodes[ 1 ] />
  •  
  •  
  • <!---
  • Now that we have our katie node, we need to get our XML Document
  • object. We can do this in two ways - using pure XPath or using a
  • combination of XPath and node traversal. In either case, the
  • trick is to remember that our XML Doc is the parent object of
  • the root node (xmlRoot).
  • --->
  •  
  • <!---
  • Get the doc using just XPath. In this case, we are going to go
  • to our root node and then get its parent.
  • --->
  • <cfset xmlDocNodes = xmlSearch( katieNodes[ 1 ], "/*/.." ) />
  •  
  • <!---
  • Get the doc using XPath and xmlParent. In this case, we'll use
  • XPath to get the root and then xmlParent to get the doc.
  •  
  • NOTE: See output for further analysis.
  • --->
  • <cfset xmlRootNodes = xmlSearch( katieNodes[ 1 ], "/*" ) />
  •  
  •  
  • <!--- Check to make sure both approaches result in an xml doc. --->
  • <cfoutput>
  •  
  • xmlDocNodes: #isXmlDoc( xmlDocNodes[ 1 ] )#
  •  
  • <br />
  • <br />
  •  
  • xmlRootNodes: #isXmlDoc( xmlRootNodes[ 1 ].xmlParent )#
  •  
  • </cfoutput>

This demo uses two approaches for obtaining the XML document reference. Both use xmlSearch(); but, each gets the XML document reference in a slightly different way. The first uses the ".." path traversal directive to jump from the root XML element to the XML document reference; the second uses only the "/*" traversal directive and then uses the xmlParent property to get the XML document reference (much like our second demo). When we run this code, we can see that both approaches work:

xmlDocNodes: YES

xmlRootNodes: YES

We now know that we can get the XML document reference based on any XML element; but, this is kind of sloppy. As a last step in our exploration, let's encapsulate this logic in a nice ColdFusion user defined function (UDF), xmlGetDoc():

  • <cffunction
  • name="xmlGetDoc"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I get the XML docuemnt reference that contains the given XML node.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="node"
  • type="any"
  • required="true"
  • hint="I am the XML node for which we are getting the parent XML document."
  • />
  •  
  • <!--- Search the document nodes. --->
  • <cfset var xmlDocNodes = xmlSearch( arguments.node, "/*/.." ) />
  •  
  • <!--- Return the XML document reference. --->
  • <cfreturn xmlDocNodes[ 1 ] />
  • </cffunction>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Create a simple XML document. --->
  • <cfxml variable="girls">
  •  
  • <girls>
  • <girl>
  • <name>Katie</name>
  • </girl>
  • <girl>
  • <name>Kim</name>
  • </girl>
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!---
  • Get the XMl Document based on an XML node. Notice that we are
  • testing two cases - an XML node and an XML document. Since
  • xmlSearch() works on both types of reference, we should be able
  • to handle both use cases transparently.
  • --->
  • <cfoutput>
  •  
  • isXmlDoc: #isXmlDoc(
  • xmlGetDoc( girls.xmlRoot.girl[ 1 ] )
  • )#
  •  
  • <br />
  • <br />
  •  
  • isXmlDoc: #isXmlDoc(
  • xmlGetDoc( girls )
  • )#
  •  
  • </cfoutput>

Notice in this demo that we are testing our xmlGetDoc() UDF with both an XML element reference and an XML document reference. Since xmlSearch() works with both types of values, our xmlGetDoc() UDF does not need to care about the subtle differences between XML documents and XML nodes. When we run the above code, we get the following output:

isXmlDoc: YES

isXmlDoc: YES

As you can see, we were able to obtain the XML document reference from both an existing XML node and an existing XML document value.

To me, it seems like a shortcoming that the xmlElemNew() function cannot create a new XML element based on any existing XML node reference. Since XML nodes can be searched, aggregated, and passed by reference, it would make more sense for the xmlElemNew() function to be more flexible. However, until the behavior of xmlElemNew() changes, at least we can manually locate the parent XML document.




Reader Comments

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.