Deleting XML Node Arrays From A ColdFusion XML Document

Posted May 23, 2008 at 8:21 AM 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

Mar 5, 2009 at 6:45 PM // reply »
50 Comments

thank you - ive been looking for this info everywere. You have a great site


Mar 5, 2009 at 6:47 PM // reply »
50 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.


Mar 5, 2009 at 7:01 PM // reply »
10,640 Comments

@James,

Awesome man, glad to help (I edited your first URL).


Mar 6, 2009 at 12:24 PM // reply »
50 Comments

Is coldfusion 8 only ? i keep getting this error

Variable XMLDELETENODES is undefined.


Mar 6, 2009 at 1:17 PM // reply »
10,640 Comments

@James,

Are you including the UDF before you run the code?


Mar 6, 2009 at 5:33 PM // reply »
50 Comments

@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


Mar 6, 2009 at 5:35 PM // reply »
10,640 Comments

@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()


Apr 21, 2009 at 5:44 PM // reply »
1 Comments

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


May 3, 2009 at 3:07 PM // reply »
10,640 Comments

@Brhine,

Awesome! Glad you are liking it.


Don
Sep 30, 2009 at 4:14 PM // reply »
57 Comments

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.


Don
Sep 30, 2009 at 4:42 PM // reply »
57 Comments

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. :)


Oct 1, 2009 at 8:01 AM // reply »
10,640 Comments

@Don,

No problem my man :) We all have our moments. Name spaces are so frustrating in XML!


Feb 17, 2010 at 12:40 PM // reply »
1 Comments

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.


Feb 17, 2010 at 1:58 PM // reply »
10,640 Comments

@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?


Aug 23, 2010 at 1:19 AM // reply »
2 Comments

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.


Aug 23, 2010 at 9:22 PM // reply »
10,640 Comments

@Angel,

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


Nov 5, 2010 at 6:13 PM // reply »
9 Comments

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>


Nov 7, 2010 at 7:48 PM // reply »
10,640 Comments

@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"
  • ) />


Oct 13, 2011 at 12:07 PM // reply »
1 Comments

@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



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
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 12, 2012 at 3:37 AM
Learning ColdFusion 8: CFImage Part III - Watermarks And Transparency
Hi Ben, Just to ask currently it is placed bottom right corner, if i need to replace the same rendered image on the bottom left side or in the bottom center, how that can be calculated. bottom ce ... read »
Feb 11, 2012 at 9:29 PM
Use jQuery's SlideDown() With Fixed-Width Elements To Prevent Jumping
I can't say how glad I am that I found your post. Thank you very much. ... read »
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »