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 cf.Objective() 2013 (Bloomington, MN) with:

Thoughts About XDOM For Easier ColdFusion XML Manipulation

By Ben Nadel on
Tags: ColdFusion

XML is some really great stuff; but, sometimes, navigating and then manipulating a XML document in ColdFusion is not always the easiest thing to do. As such, I wanted to try creating a ColdFusion component that could act as a wrapper to an XML document, exposing an easier-to-use API for traversal, manipulation, and merging. For the time being, I am calling this ColdFusion component, "XDOM." I originally wanted to call it "xQuery," in homage to jQuery; but, XQuery is already a prominent name in the XML world.

Similar to jQuery, XDOM encapsulates a collection of ColdFusion XML nodes (element nodes and attribute nodes). When you create an XDOM instance, you don't necessarily need to pass in this collection. Sure, you can pass in an XML document or a collection of nodes; but, you can also pass in an XML string or an existing XDOM instance:

  • XDOM.init( xmlString )
  • XDOM.init( xmlDoc )
  • XDOM.init( xmlNode )
  • XDOM.init( arrayOfXmlNodes )
  • XDOM.init( XDOMInstance )

Once you have your XDOM instance, you can then begin to traverse and manipulate the composed collection. At the time of this writing, XDOM exposes only two traversal methods:

  • XDOM.find( xpathQuery )
  • XDOM.end()

The find() method takes an XPath query which will be executed on every top-level element within the current collection. The results of each internal xmlSearch() request are then compiled into a single, unique collection and returned as a new XDOM instance.

Whenever you call find(), a new XDOM instance is returned. As part of this operation, the current collection is linked to the new collection in such a way that calling end() on the new collection will return the current collection. As such, the following traversal path will bring you back to the original XDOM instance:

  • xdomInstance.find( "//someNode" ).end()

Once you have located the parts of the XML document that you want to use, XDOM exposes a few access and manipulation methods:

  • XDOM.append( initArgs [, returnAppendedElements] )
  • XDOM.get( [index] )
  • XDOM.getAttributeArray( attributeName )
  • XDOM.getAttributeList( attributeName [, delimiter] )
  • XDOM.getValueArray()
  • XDOM.getValueList( [delimiter] )
  • XDOM.remove()

The append() method will copy the incoming collection to every element node in the current collection. The incoming collection can be any format supported by the init() method. By default, the append() collection just returns the current XDOM instance for method chaining; however, if you pass it an optional secondary argument, returnAppendedElements, it will create a new collection containing the newly appended elements and return that.

The getAttributeArray() and getAttributeList() methods aggregate the values of a given attribute contained in each top-level node in the collection.

The getValueArray() and getValueList() methods aggregate the text values of each top-level node in the collection.

The remove() method detaches the current collection from its parent XML document.

That's all I have at the moment. Let's take a look at XDOM in action to get a better sense of how it might be used. Keep in mind that not all of the following code is necessarily practical - I'm just trying to demonstrate how the XDOM API can be leveraged.

  • <!--- Create some test XML data that we can manipulate with XDOM. --->
  • <cfxml variable="data">
  •  
  • <women />
  •  
  • </cfxml>
  •  
  • <!--- Create an XDOM wrapper instance for our XML data. --->
  • <cfset women = createObject( "component", "XDOM" ).init( data ) />
  •  
  •  
  • <!--- ---------------------------------------------------- --->
  • <!--- ---------------------------------------------------- --->
  •  
  •  
  • <!---
  • Here, we are going to demonstrate the append with strings.
  • XDOM will automatically parse XML and append it to the current
  • collection. You can pass an optional second argument to append()
  • to have it return the newly appended elements.
  •  
  • As you can see, end() pops you back up to the previous collection
  • as it does with jQuery.
  • --->
  • <cfset women
  • .append( "<woman />", true )
  • .append( "<name isSingle='true'>Sarah</name>" )
  • .append( "<age>31</age>" )
  • .end()
  • .append( "<woman />", true )
  • .append( "<name isSingle='false'>Tricia</name>" )
  • .append( "<age>33</age>" )
  • .end()
  • />
  •  
  •  
  • <!--- ---------------------------------------------------- --->
  • <!--- ---------------------------------------------------- --->
  •  
  •  
  • <!--- Now, let's create another XML document. --->
  • <cfxml variable="newWoman">
  •  
  • <woman>
  • <name>Jennifer</name>
  • <age>35</age>
  • <isHot>true</isHot>
  • </woman>
  •  
  • </cfxml>
  •  
  • <!--- Create a new XDOM collection. --->
  • <cfset jenn = createObject( "component", "XDOM" ).init( newWoman ) />
  •  
  •  
  • <!---
  • Let's append our Jenn to our existing women. Notice that these
  • are two DIFFERENT XML documents with different owners. Because
  • of this, Jenn is actually "copied" into the existing three,
  • *not* imported. Since ColdFusion does not expose an import
  • natively (you can get to it via Java), I wanted to resort to
  • copying.
  • --->
  • <cfset women.append( jenn ) />
  •  
  •  
  • <!--- ---------------------------------------------------- --->
  • <!--- ---------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now, let's say we changed our mind about Jenn's hotness. We
  • want to find the isHot node, remove it, and append it to Tricia.
  • --->
  • <cfset women
  • .find( "//woman[ name/text() = 'Tricia' ]" )
  • .append(
  • women.find( "//isHot" ).remove()
  • )
  • />
  •  
  •  
  • <!--- ---------------------------------------------------- --->
  • <!--- ---------------------------------------------------- --->
  •  
  •  
  • <!--- Output our resultant node tree. --->
  • <cfdump
  • var="#women.get( 1 )#"
  • label="Women XML"
  • />

As you can see, we've got two different XML documents in play here. XDOM is being used to traverse, manipulate, and merge these two XML trees. When we run the above code, we get the following CFDump output of our resultant XML tree:


 
 
 

 
XDOM Is A ColdFusion Component Wrapper For XML Traversal, Manipulation, And Merging.  
 
 
 

When XDOM appends XML nodes to its composed collection, it does so through duplication. That is, it manually duplicates the external nodes within each of its composed nodes before it appends them. This is done for a few reasons:

  1. You might be appending a single node to multiple composed nodes. In such a case, there is no way to perform that append() without node duplication.
  2. ColdFusion does not expose any native "import" for XML nodes. In the underlying Java XML DOM (which ColdFusion wraps), there is a sense of node ownership; that is, an XML node belongs to one and only one XML DOM. As such, you can't simply move an XML node from one XML DOM to another; doing so will result in a "wrong owner" error.
  3. ColdFusion inserts XML nodes by duplication anyway. As such, appending an XML node to an XML tree already breaks any existing reference to the inserted XML node.

Let's also take a quick look at some of the "get" methods which can be used to aggregate attribute values and node text:

  • <!--- Now, let's get some data about our XML tree. --->
  • <cfoutput>
  •  
  • <!--- Get the names of the girls. --->
  • Women:
  • #women.find( "//name" ).getValueList()#
  •  
  • <br />
  • <br />
  •  
  • <!--- Get the aveage age of the girls. --->
  • Average Age:
  • #arrayAvg( women.find( "//age" ).getValueArray() )#
  •  
  • </cfoutput>

Running this code gives us the following output:

Women: Sarah,Tricia,Jennifer

Average Age: 33

So that's all I've got so far. If you have any thoughts about features you'd like to see in an XML manipulation wrapper, I'd love to hear them. If people find this idea to be useful, I can wrap it up as a project and publish it.




Reader Comments

Interesting! This is not immediately useful to me, but (like so many other of your ideas) I'm bookmarking it in anticipation of possible usefulness. Thanks!

Wow! This looks really nice. I have definitely found a few area where ColdFusion doesn't make XML as nice as it could and it looks like you have really made some nice jumps on that.

I have used you getValueList() in the past and really like it. As you mentioned, adding nodes to XML documents can sometimes be a bit hinky, so this could help with that issue as well.

Two ideas right off the bat.

1) Could you allow for a full file path as one of the options for the "init" argument? That would save the calling code the trouble of loading the file in memory before calling XDOM (a small thing, admittedly).

2) I would love something where I could take some action against each result of an XPath statement by passing in a function.

Maybe do(xpath,function)

I don't have this fully thought out though.

@Matt,

Ha ha, thanks :) I know what you mean though. I started this on my train right back from RIA Unleashed. I didn't have a use in mind; it's just been something I've thought about doing from time to time.

@Steve,

I like your suggestions. The file-read thing should not be a problem at all. The "each" style functionality should also be fairly easy. The only pain in ColdFusion is that you can't create inline functions; however, if you use the CFFunction tag (or likewise), there's no functional reason that wouldn't work.

One thing that would make this really sexy (Though might not be what you're going for). Would be to have a selector translation, so you could enter a CSS3 selector, like "women woman[ishot=true]".. and have it translated into Xpath, and do the selection.

The first thing that comes to might with this ease of XML manipulation would be to take you rendered HTML content, and massage it before display.. though I can't think of when this method would work better then just inline CF (unless you scraped it from another site.. oooo yeah, then you can pick it apart and display it how ever you want, kinda like YQL).

@All,

I posted the code for this on its project page if anyone is interested:

http://www.bennadel.com/projects/xdom.htm

@Steve,

I added the ability to read in a file if you pass in a file path to the init() method. I am thinking about the function iteration since that could perhaps be done more easily with something like:

<cfloop array="#xdom.get()#"> ... </cfloop>

@Tim,

Hmm, CSS is an interesting idea. CSS is very close to XPath. I am not sure if it would be worth the overhead of having to perform the internal conversion. The benefit would be, of course, you don't have to learn XPath if you already know CSS. XPath does give you a lot more control, however.

I'll give it some thought.

Being able to massage HTML into XHTML, however is a cool thought. Cleaning HTML can be a difficult task. I know there are libraries that handle that. I'll do some more thinking.

How about a custom tag to simplify the cfxml/createObject process:

  • <cfimport prefix="xdom" taglib="/path/to/xdom" />
  •  
  • <xdom:init variable="women">
  • <women/>
  • </xdom:init>
  •  
  • <xdom:init variable="jenn">
  • <woman>
  • <name>Jennifer</name>
  • <notage>35</notage>
  • <isHot>true</isHot>
  • </woman>
  • </xdom:init>

Could also consider an xdom:append tag for appending complex chunks.

Another thing that might be handy is an overloaded append that lets you do:

  • women.append
  • ( Tag = 'name'
  • , Attributes = { isSingle='false' }
  • , Content = 'Tricia'
  • )

Btw, that <notage> is because otherwise I get "Your comment contains restricted HTML elements (A)." - might want to double check the validation code.