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 Scotch On The Rocks (SOTR) 2011 (Edinburgh) with:

Deleting XML Node Arrays From A ColdFusion XML Document

Posted by Ben Nadel
Tags: ColdFusion

Yesterday, I was working on an audit system that stored data at XML when I needed to delete a bunch of XML nodes from a particular document. I don't spend a lot of time manipulating ColdFusion XML documents; most of my work with them has been either creating them and searching them using XPath and XmlSearch(). As such, I wasn't sure what the best way to go about it was. In the past, I have used ArrayDeleteAt() to remove a single XML node from an XmlChildren array. This, however requires you to have both the parent node and the tree index of the node being deleted. Unfortunately, all I had was an array of nodes retrieved via XmlSearch(); as such, I had neither the parent node nor the index of each given node in its original context.

After some quick Google searching, I realized that there was nothing out there that would handle this kind of anonymous, mass node deletion for me; and so, I joyfully took the opportunity to write one for myself. I call it XmlDeleteNodes() and it can take either a single XML node or an array of XML nodes that need to be deleted from the given document:

XmlDeleteNodes( XmlDocument, ( XmlNode | XmlNodeArray ) ) :: Void

Before we see how this works, let's take a look at it in action. Here, we are going to build a ColdFusion XML document that has girls with various hair colors. Then, we are going to gather all the girl nodes who have a Blonde hair descendant node. Then, we are going to delete them from the original ColdFusion XML document:

  • <!--- Create a ColdFusion XML document. --->
  • <cfxml variable="xmlGirls">
  •  
  • <girls>
  • <girl id="1">
  • <name>Hayden Panettiere</name>
  • <hair>Blonde</hair>
  • </girl>
  • <girl id="2">
  • <name>Christina Cox</name>
  • <hair>Blonde</hair>
  • </girl>
  • <girl id="3">
  • <name>Winona Ryder</name>
  • <hair>Brunette</hair>
  • </girl>
  • <girl id="4">
  • <name>Minnie Driver</name>
  • <hair>Brunette</hair>
  • </girl>
  • <girl id="5">
  • <name>Julia Stiles</name>
  • <hair>Blonde</hair>
  • </girl>
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!--- Query for all blonde xml nodes. --->
  • <cfset arrBlondeGirls = XmlSearch(
  • xmlGirls,
  • "//girl[ hair/text() = 'Blonde' ]"
  • ) />
  •  
  • <!--- Delete all blondes from the document. --->
  • <cfset XmlDeleteNodes(
  • xmlGirls,
  • arrBlondeGirls
  • ) />
  •  
  • <!---
  • Output the modified XML document, which should, at
  • this point, only contain Brunetted.
  • --->
  • <cfdump
  • var="#xmlGirls#"
  • label="xmlGirls - After Blondes Have Been Removed"
  • />

Here, we are using XPath and XmlSearch() to get an array of pointers to the Blonde girl XML Nodes. Then, we pass that array of nodes to XmlDeleteNodes(). After that, our Girls XML document looks like this:


 
 
 

 
ColdFusion XML Document After Nodes Were Deleted Using XmlDeleteNodes()  
 
 
 

Notice that the only two nodes we have left are the Girl nodes that have descendant Brunette text nodes (now that's what I call a party ;)). I love the fact that I don't have to worry about indexes or parent nodes - the XmlDeleteNodes() just takes care of that for me; this makes it very easy to harness the extremely awesome power of XmlSearch() and XPath.

Ok, so now let's take a look at the function behind the scenes:

  • <cffunction
  • name="XmlDeleteNodes"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I remove a node or an array of nodes from the given XML document.">
  •  
  • <!--- Define arugments. --->
  • <cfargument
  • name="XmlDocument"
  • type="any"
  • required="true"
  • hint="I am a ColdFusion XML document object."
  • />
  •  
  • <cfargument
  • name="Nodes"
  • type="any"
  • required="false"
  • hint="I am the node or an array of nodes being removed from the given document."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!---
  • Check to see if we have a node or array of nodes. If we
  • only have one node passed in, let's create an array of
  • it so we can assume an array going forward.
  • --->
  • <cfif NOT IsArray( ARGUMENTS.Nodes )>
  •  
  • <!--- Get a reference to the single node. --->
  • <cfset LOCAL.Node = ARGUMENTS.Nodes />
  •  
  • <!--- Convert single node to array. --->
  • <cfset ARGUMENTS.Nodes = [ LOCAL.Node ] />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Flag nodes for deletion. We are going to need to delete
  • these via the XmlChildren array of the parent, so we
  • need to be able to differentiate them from siblings.
  • Also, we only want to work with actual ELEMENT nodes,
  • not attributes or anything, so let's remove any nodes
  • that are not element nodes.
  • --->
  • <cfloop
  • index="LOCAL.NodeIndex"
  • from="#ArrayLen( ARGUMENTS.Nodes )#"
  • to="1"
  • step="-1">
  •  
  • <!--- Get a node short-hand. --->
  • <cfset LOCAL.Node = ARGUMENTS.Nodes[ LOCAL.NodeIndex ] />
  •  
  • <!---
  • Check to make sure that this node has an XmlChildren
  • element. If it does, then it is an element node. If
  • not, then we want to get rid of it.
  • --->
  • <cfif StructKeyExists( LOCAL.Node, "XmlChildren" )>
  •  
  • <!--- Set delet flag. --->
  • <cfset LOCAL.Node.XmlAttributes[ "delete-me-flag" ] = "true" />
  •  
  • <cfelse>
  •  
  • <!---
  • This is not an element node. Delete it from out
  • list of nodes to delete.
  • --->
  • <cfset ArrayDeleteAt(
  • ARGUMENTS.Nodes,
  • LOCAL.NodeIndex
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • Now that we have flagged the nodes that need to be
  • deleted, we can loop over them to find their parents.
  • All nodes should have a parent, except for the root
  • node, which we cannot delete.
  • --->
  • <cfloop
  • index="LOCAL.Node"
  • array="#ARGUMENTS.Nodes#">
  •  
  • <!--- Get the parent node. --->
  • <cfset LOCAL.ParentNodes = XmlSearch( LOCAL.Node, "../" ) />
  •  
  • <!---
  • Check to see if we have a parent node. We can't
  • delete the root node, and we also be deleting other
  • elements as well - make sure it is all playing
  • nicely together. As a final check, make sure that
  • out parent has children (only happens if we are
  • dealing with the root document element).
  • --->
  • <cfif (
  • ArrayLen( LOCAL.ParentNodes ) AND
  • StructKeyExists( LOCAL.ParentNodes[ 1 ], "XmlChildren" )
  • )>
  •  
  • <!--- Get the parent node short-hand. --->
  • <cfset LOCAL.ParentNode = LOCAL.ParentNodes[ 1 ] />
  •  
  • <!---
  • Now that we have a parent node, we want to loop
  • over it's children to one the nodes flagged as
  • deleted (and delete them). As we do this, we
  • want to loop over the children backwards so that
  • we don't go out of bounds as we start to remove
  • child nodes.
  • --->
  • <cfloop
  • index="LOCAL.NodeIndex"
  • from="#ArrayLen( LOCAL.ParentNode.XmlChildren )#"
  • to="1"
  • step="-1">
  •  
  • <!--- Get the current node shorthand. --->
  • <cfset LOCAL.Node = LOCAL.ParentNode.XmlChildren[ LOCAL.NodeIndex ] />
  •  
  • <!---
  • Check to see if this node has been flagged
  • for deletion.
  • --->
  • <cfif StructKeyExists( LOCAL.Node.XmlAttributes, "delete-me-flag" )>
  •  
  • <!--- Delete this node from parent. --->
  • <cfset ArrayDeleteAt(
  • LOCAL.ParentNode.XmlChildren,
  • LOCAL.NodeIndex
  • ) />
  •  
  • <!---
  • Clean up the node by removing the
  • deletion flag. This node might still be
  • used by another part of the program.
  • --->
  • <cfset StructDelete(
  • LOCAL.Node.XmlAttributes,
  • "delete-me-flag"
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>

I came up with this function to help me harness the power of XPath and, not surprisingly, the XmlDeleteNodes() function itself harness the power of XPath to perform the delete XML node actions. The magic here comes from the fact that given a single XML Node, you can use XPath and XmlSearch() to actually get that node's parent node. Then, from there, all it takes is a little attribute flag and a child node iteration to find the node you want to delete. Piece of cake.

When I first started this, I thought maybe I would just use the underlying Java goodness that is the Xerces library. Using the Java methods, it is easy to get the parent node using the .GetParentNode() method. Then, the parent node has a .RemoveChild( ChildNode ) method. This would have been the easiest route, but dammed if I couldn't figure out how to convert my ColdFusion Node pointer to a org.w3c.dom Node. If I tried to just pass in the ColdFusion Node (child node), it couldn't match up the method signature and it kept telling me that the method could not be found.

I could have done NODE.GetPreviousSibling().GetNextSibling() to get the Java node, but there is part of me that is happy to have a ColdFusion-only solution. Plus, I think it's pretty efficient; there is more XML tree traversal than direct node deletion, but that is kept to a minimum since only the siblings of the target nodes are searched.

.... however, if you love messing under the hood and you want to see what that Java method would have looked like, here it is:

  • <cffunction
  • name="XmlDeleteNodesJava"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I remove a node or an array of nodes from the given XML document.">
  •  
  • <!--- Define arugments. --->
  • <cfargument
  • name="XmlDocument"
  • type="any"
  • required="true"
  • hint="I am a ColdFusion XML document object."
  • />
  •  
  • <cfargument
  • name="Nodes"
  • type="any"
  • required="false"
  • hint="I am the node or an array of nodes being removed from the given document."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!---
  • Check to see if we have a node or array of nodes. If we
  • only have one node passed in, let's create an array of
  • it so we can assume an array going forward.
  • --->
  • <cfif NOT IsArray( ARGUMENTS.Nodes )>
  •  
  • <!--- Get a reference to the single node. --->
  • <cfset LOCAL.Node = ARGUMENTS.Nodes />
  •  
  • <!--- Convert single node to array. --->
  • <cfset ARGUMENTS.Nodes = [ LOCAL.Node ] />
  •  
  • </cfif>
  •  
  •  
  • <!--- Loop over the nodes. --->
  • <cfloop
  • index="LOCAL.Node"
  • array="#ARGUMENTS.Nodes#">
  •  
  • <!--- Get the parent node. --->
  • <cfset LOCAL.ParentNode = LOCAL.Node.GetParentNode() />
  •  
  • <!---
  • Check to see if the parent was found. If not, then
  • we are not dealing with an Element node.
  • --->
  • <cfif StructKeyExists( LOCAL, "ParentNode" )>
  •  
  • <!---
  • Get the previous sibling of the node in the
  • question. If there is no previous sibling, this
  • will return NULL and it will tell us that
  • the target node is the first node in the child
  • nodes array.
  • --->
  • <cfset LOCAL.PrevNode = LOCAL.Node.GetPreviousSibling() />
  •  
  • <!---
  • Check to see if the previous node was found
  • or if is null (which will have removed the
  • struct key).
  • --->
  • <cfif StructKeyExists( LOCAL, "PrevNode" )>
  •  
  • <!---
  • We have the prev node. Use that to get the
  • Java version of our ChildNode and delete from
  • the parent.
  • --->
  • <cfset LOCAL.ParentNode.RemoveChild(
  • LOCAL.PrevNode.GetNextSibling()
  • ) />
  •  
  • <cfelse>
  •  
  • <!---
  • The previous node didn't exist which means
  • that our target node is the first node in the
  • child array.
  • --->
  • <cfset LOCAL.ParentNode.RemoveChild(
  • LOCAL.ParentNode.GetFirstChild()
  • ) />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>

As you can see, it is much much shorter and does not use any tree traversal at all. It simply deletes the XML nodes directly from the parent XML Node. I guess you could start off with the Java version, since it is encapsulated, and the switch over to the ColdFusion / XPath solution if anything goes wrong in the future.



Reader Comments

you should indicate weather or not to use http:// in website link !_!.
and even a possible edit comments within say 10mins email provided. again great site.

Reply to this Comment

@ben or any other cfml guru

now i included it - but i still get a error

Invalid CFML construct found on line 37 at column 42.
ColdFusion was looking at the following text:
[

The CFML compiler was processing:

a cfset tag beginning on line 37, column 18.
a cfset tag beginning on line 37, column 18.
a cfset tag beginning on line 37, column 18.


The error occurred in D:\Hosting\findapro\testpages\myfunc.cfm: line 37

35 :
36 : <!--- Convert single node to array. --->
37 : <cfset ARGUMENTS.Nodes = [ LOCAL.Node ] />
38 :
39 : </cfif>


---------------------

I dont know if this is write but I just included the udf it on top of the other file?. ah im still a nub haha, Man your stuff is advanced it's great - ill figure it out. thanks

Reply to this Comment

@James,

That's a CF8 error. If you are running CF7 you will have to change the implicit struct / array notation to the standard one.

[] becomes ArrayNew( 1 )
{} becomes StructNew()

Reply to this Comment

Just found this today; manymany thanks, as it's made my afternoon MUCH smoother, and me a very happy camper. Works like a charm.

Reply to this Comment

Got anything for deleting xmlattributes?
I was beating my head against my chair (I'd slipped to the floor in dismay) because this xml feed would not transform. So I just captured the xml into a file so I could see what it really is and then I cut it down to a managable size (about 5 records) and then started playing with just that.
In the top level node is an xmlns xmlattribute. I removed that and the transform worked.
So it seems simple, just do a structdelete to remove it. Not so fast! After doing that I got errors. It seems it changed my entire xml doc to one word. "YES". huh?
I could go on and on but I'm getting desperate (or hungry). I hate wasting so much time on something that should be sooooo simple.

Reply to this Comment

Oh darn. Can I delete this comment?
Is it Friday? My brain definitly shut down on this one.
Here is what I was doing
<cfset myXML = StructDelete(myXML.rootnode.XmlAttributes,"xmlns")>

Yup. Excuse me while I smack my head with a 2 by 4. sheeesh.
(for any other dunderheads out there here is the RIGHT way)
<cfscript>
StructDelete(myXML.rootnode.XmlAttributes,"xmlns");
</script>
I'm sure you can do it outside the cfscript but that just was easier for me.
Probably save it as a temp var.
Setting the xml variable to the results of the StructDelete just tells you if the delete worked.
*sigh*
One day I will be smart like Ben. :)

Reply to this Comment

It appears that you can only remove nodes from the XML document that you performed the xmlSearch on. Also, the cfargument XMLDocument is not required in the function because LOCAL.ParentNodes = XmlSearch( LOCAL.Node, "../" ) is not searching the passed in XMLDocument.

What would you do if you wanted to query XML document A for the nodes that you wanted to delete from XML document B?

Thanks. Awesome site.

Reply to this Comment

@Andy,

I am not sure what you are trying to do? Rather than searching in two different documents, why not just search in the one you're trying to mutate?

Can you give me a little more of a use-case?

Reply to this Comment

Hi Ben - I've been trying to find a way to Delete a single contact in Google Contacts using their API. I noticed that the only query param you can really pass is the last modified record. How impractical - why would you want to delete that one.

I was wondering if you, or someone you have worked with has created a ColdFusion solution for deleting or modifying Google Contacts.

Thanks in advance.

Reply to this Comment

@Angel,

I haven't tried anything with Google Contacts. Is that part of Gmail? Or are you referring to something else?

Reply to this Comment

Thanks for the cffunction. It helped me on a project where I needed to "delete" and "extract html div tag" from an xhtml file.

Since your function does not support deleting an xml attribute, I just emptied the attribute using the following code:

<!--- Search for the: "onload" attribute of the <body> XML node using XPath. --->

<cfset arrSearchNodes= XmlSearch(
xmldocObj,
"//body[@onload]" ) />

<cfif arraylen(arrSearchNodes ) gte 1>

<!--- Delete specified attribute from the document. --->

<cfset arrSearchNodes[1].XmlAttributes['onload']= "" >

</cfif>

Reply to this Comment

@Dangle,

No problem. You should also be able to use structDelete() on the XmlAttributes structure to physically remove the attribute (rather than just setting it to the empty string):

  • <cfset structDelete(
  • arrSearchNodes[1].XmlAttributes,
  • "onload"
  • ) />

Reply to this Comment

@Ben,

I am having the same problem unfortunately. I created a master XML doc (A), copied it to a new XML variable to modify (B), and am searching and destroying any nodes in (B) that are found in a log XML (C). Each of these XML docs are based off of the same node structure and naming convention. When I run the UDF, it deletes the correct nodes, but in every XML doc (yikes!). These XML vars are named differently, but apparently the UDF is not modifying the passed XML doc, but every doc that shares the same formatting (A, B, and C).

The UDF works beautifully, but my guess is only if each XML doc is completely unique.

Chris

Reply to this Comment

@ChrisS, @Chris,

I believe all the XML document / nodes are passed by reference. If you want to make unique edits to one version, make sure to duplicate() it first.

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.