Copy / Import XML Nodes Into A ColdFusion XML Document

Posted March 18, 2009 at 10:04 AM

Tags: ColdFusion

A long time ago, I described how to append XML nodes from one ColdFusion XML document into another ColdFusion XML document. My solution, at the time, required you to use the underlying Java methods of the ColdFusion XML document. As much as I like the concept of leveraging the underlying Java power, I do like my solutions to be as ColdFusion friendly as possible. As such, I wanted to see if I could come with a ColdFusion-only solution to the problem of importing XML nodes from one ColdFusion XML document into another ColdFusion XML document. Also, while the previous solution imported and appended in one call, I wanted my new solution to be only the import part such that the resultant nodes could be manipulated independently.

I thought about the kind of use cases in which this would be useful and I figured that I would most likely be passing in a given XML node tree (or sub-tree) or an array of nodes. A given node seemed like it would be the easiest since you can duplicate it and then recursively import its children. An array of nodes on the other hand is a bit more complicated because each array index could be a sub-tree of another index in the same array. For example, if you did an XmlSearch() for:

//*

The first node returned might be the parent node of the second node returned, in which case the first node would contain references to the second node. As you can see, when dealing with arrays of node references, there can be a lot of overlapping. Unfortunately, I could not come up with a way to handle this overlapping well. As such, when you import an array of nodes, each array index is treated as a completely separate tree.

The resultant ColdFusion user defined function, XmlImport(), handles the above two use cases:

 Launch code in new window » Download code as text file »

  • <cffunction
  • name="XmlImport"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I import the given XML data into the given XML document so that it can inserted into the node tree.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="ParentDocument"
  • type="xml"
  • required="true"
  • hint="I am the parent XML document into which the given nodes will be imported."
  • />
  •  
  • <cfargument
  • name="Nodes"
  • type="any"
  • required="true"
  • hint="I am the XML tree or array of XML nodes to be imported. NOTE: If you pass in an array, each array index is treated as it's own separate node tree and any relationship between node indexes is ignored."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  •  
  • <!---
  • Check to see how the XML nodes were passed to us. If it
  • was an array, import each node index as its own XML tree.
  • If it was an XML tree, import recursively.
  • --->
  • <cfif IsArray( ARGUMENTS.Nodes )>
  •  
  • <!--- Create a new array to return imported nodes. --->
  • <cfset LOCAL.ImportedNodes = [] />
  •  
  • <!--- Loop over each node and import it. --->
  • <cfloop
  • index="LOCAL.Node"
  • array="#ARGUMENTS.Nodes#">
  •  
  • <!--- Import and append to return array. --->
  • <cfset ArrayAppend(
  • LOCAL.ImportedNodes,
  • XmlImport(
  • ARGUMENTS.ParentDocument,
  • LOCAL.Node
  • )
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Return imported nodes array. --->
  • <cfreturn LOCAL.ImportedNodes />
  •  
  • <cfelse>
  •  
  • <!---
  • We were passed an XML document or nodes or XML string.
  • Either way, let's copy the top level node and then
  • copy and append any children.
  •  
  • NOTE: Add ( ARGUMENTS.Nodes.XmlNsURI ) as second
  • argument if you are dealing with name spaces.
  • --->
  • <cfset LOCAL.NewNode = XmlElemNew(
  • ARGUMENTS.ParentDocument,
  • ARGUMENTS.Nodes.XmlName
  • ) />
  •  
  • <!--- Append the XML attributes. --->
  • <cfset StructAppend(
  • LOCAL.NewNode.XmlAttributes,
  • ARGUMENTS.Nodes.XmlAttributes
  • ) />
  •  
  • <!--- Copy simple values. --->
  • <!---
  • <cfset LOCAL.NewNode.XmlNsPrefix = ARGUMENTS.Nodes.XmlNsPrefix />
  • <cfset LOCAL.NewNode.XmlNsUri = ARGUMENTS.Nodes.XmlNsUri />
  • --->
  • <cfset LOCAL.NewNode.XmlText = ARGUMENTS.Nodes.XmlText />
  • <cfset LOCAL.NewNode.XmlComment = ARGUMENTS.Nodes.XmlComment />
  •  
  • <!---
  • Loop over the child nodes and import them as well
  • and then append them to the new node.
  • --->
  • <cfloop
  • index="LOCAL.ChildNode"
  • array="#ARGUMENTS.Nodes.XmlChildren#">
  •  
  • <!--- Import and append. --->
  • <cfset ArrayAppend(
  • LOCAL.NewNode.XmlChildren,
  • XmlImport(
  • ARGUMENTS.ParentDocument,
  • LOCAL.ChildNode
  • )
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Return the new, imported node. --->
  • <cfreturn LOCAL.NewNode />
  •  
  • </cfif>
  • </cffunction>

In the above UDF, I have the namespace functionality commented out because I never use XML namespaces. However, if you wanted to add it, you would just need to uncomment the two properties and change the XmlElemNew() method call. Unfortunately, there was no way that I could find to directly import an XML node, so, if you look at the recursive nature of the XmlImport() function, you will see that it is actually building a mirror copy of the node, not technically importing it. While less than elegant, the end result in the same.

To test this, I set up two ColdFusion XML documents:

 Launch code in new window » Download code as text file »

  • <!--- Build one ColdFusion XML document. --->
  • <cfxml variable="xmlGirls">
  •  
  • <girls>
  • <girl id="1">
  • <name>Molly</name>
  • <best>Smile</best>
  • </girl>
  • <girl id="2">
  • <name>Sarah</name>
  • <best>Legs</best>
  • </girl>
  • </girls>
  •  
  • </cfxml>
  •  
  •  
  • <!--- Build another ColdFusion XML document. --->
  • <cfxml variable="xmlGirls2">
  •  
  • <girls>
  • <girl id="3">
  • <name>Libby</name>
  • <best>Hair</best>
  • </girl>
  • <girl id="4">
  • <name>Maria</name>
  • <best>Attitude</best>
  • </girl>
  • </girls>
  •  
  • </cfxml>

Now, that we have two XML documents set up, let's try to copy the nodes directly (to make sure the wrong way still doesn't work):

 Launch code in new window » Download code as text file »

  • <!---
  • We want to append the girls from the second XML document
  • into girls of the first XML document. To do so, iterate
  • over the girls and append each one.
  • --->
  • <cfloop
  • index="xmlGirl"
  • array="#xmlGirls2.XmlRoot.XmlChildren#">
  •  
  • <!--- Append to first XML document. --->
  • <cfset ArrayAppend(
  • xmlGirls.XmlRoot.XmlChildren,
  • xmlGirl
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Output resultant XML tree. --->
  • <cfdump
  • var="#xmlGirls#"
  • label="xmlGirls (Merged Tree)"
  • />

As expected, when we run this, ColdFusion will not allow use to copy one node directly into another XML document and throws the following error:

WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it. null

To get around this, we need to import the nodes from the second XML document into the first XML document before we can use them:

 Launch code in new window » Download code as text file »

  • <!---
  • We want to append the girls from the second XML document
  • into girls of the first XML document. To do so, iterate
  • over the girls and append each one.
  •  
  • NOTE: Since these are two different XML documents, we have
  • to import the XML node set before we iterate over it.
  • --->
  • <cfset arrImportedNodes = XmlImport(
  • xmlGirls,
  • xmlGirls2.XmlRoot.XmlChildren
  • ) />
  •  
  • <!---
  • Loop over imported nodes and insert them into the XML DOM
  • (of their newly assigned parent).
  • --->
  • <cfloop
  • index="xmlGirl"
  • array="#arrImportedNodes#">
  •  
  • <!--- Append to first XML document. --->
  • <cfset ArrayAppend(
  • xmlGirls.XmlRoot.XmlChildren,
  • xmlGirl
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Output resultant XML tree. --->
  • <cfdump
  • var="#xmlGirls#"
  • label="xmlGirls (Merged Tree)"
  • />

As you can see, importing the XML nodes from one document into another doesn't inherently do anything. The newly imported nodes are not automatically inserted into the target XML DOM; they are simply copied into the target XML document context. Once they have been imported, they can then be inserted into the new XML DOM. Running the above code, we get the following CFDump output:

 
 
 
 
 
 
XmlImport() Used to Copy And Merge Two ColdFusion XML Documents. 
 
 
 

As you can see, the girl nodes from the second XML document have been successfully imported into the first ColdFusion XML document and then inserted into the XML tree.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Other Searches  |  Print Page




Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

Mar 18, 2009 at 10:32 AM // reply »
5 Comments

Hi Ben,

I was trying to use your previous code just a week or two ago without much success. It may be because I was trying to do something it's not designed to do. Will your script allow you to import one set of nodes into another child? For example, if I have:

groups
--group
----people
------persons

Could I use your function to add more persons to the people node above?

Thanks for all you do.

Dan


Mar 18, 2009 at 10:36 AM // reply »
7,572 Comments

@Dan,

Yeah, no problem. If you wanted to, you could build up the new children in a new CFXML tag (or something like that) and then append them.

Pseudo example:

<cfxml variable="xmlNewPerson">
. . . . <person>
. . . . . . . . <name>Ben Nadel</name>
. . . . </person>
</cfxml>

<cfset xmlPerson = XmlImport( xmlExistingTree, xmlNewPerson ) />

<cfset ArrayAppend(
. . . . xmlExistingTree.groups.group.people.XmlChildren,
. . . . xmlPerson
) />

Something like that.


Mar 18, 2009 at 11:27 AM // reply »
1 Comments

Awesome Ben, I'll give it a try.

Thanks,

Dan


Mar 18, 2009 at 4:56 PM // reply »
2 Comments

Ben,

I may be missing one or two details, but would this version work?

<cffunction name="XmlImport" access="public" returntype="struct" output="false" hint="I import the given XML data into the given XML document so that it can inserted into the node tree.">
<cfargument name="ParentDocument" type="xml" required="true" />
<cfargument name="Nodes" type="any" required="true" />

<cfset var ImportedNodes = arrayNew(1) />
<cfset var NewNode = arrayNew(1) />
<cfset var returnStruct = structNew() />

<cfif NOT IsArray(arguments.Nodes)>
<cfreturn Arguments.Nodes />
<cfelse>
<cfloop array="#ARGUMENTS.Nodes#" index="Node">
<cfset ArrayAppend(ImportedNodes, XmlImport(ARGUMENTS.ParentDocument, Node)) />
</cfloop>
</cfif>

<cfset returnStruct.importedXML = ImportedNodes />

<cfreturn returnStruct />
</cffunction>

Makes it a bit shorter and cleaner, methinks -- I could be missing something with the recursion though -- I seem to remember something about recursion being most efficient when the recursive call is the last thing that gets done, but it's been a while since Comp 15 at Halligan.


Mar 18, 2009 at 6:46 PM // reply »
7,572 Comments

@Joe,

Ahhh, Halligan hall. That makes me sentimental :)

The problem with your solution is that you never actually import the node into the target XML document. If I passed in an XML node reference, your code would do this:

<cfif NOT IsArray(arguments.Nodes)>
. . . . <cfreturn Arguments.Nodes />
<cfelse>

This simply returns the reference back to the calling code. The node reference is still in the context of it's original XML parent document. The trick is that we have to create a mirrored element in the context of the target XML document, hence the XmlElemNew() call.


Mar 19, 2009 at 10:24 AM // reply »
5 Comments

Great stuff Ben - your older solution saved me oodles of work a few days ago. Cheers.


Jan 15, 2010 at 2:15 PM // reply »
4 Comments

Waow! That's good!!!


Post Comment  |  Ask Ben

Recent Blog Comments
Mar 21, 2010 at 8:57 PM
The Bourne Ultimatum Starring Matt Damon And Julia Stiles
late to the party, but my observation is this: rewatch carefully for the platonic nature of the relationship between nicki and jason. she never flirts with him. he never comes on to her. they alway ... read »
Mar 21, 2010 at 7:40 PM
Is Simulating User-Input Events With jQuery Ever A Good Idea?
A couple of things. One you embed the initial state of of more-info in the CSS. IMHO, that behavior should be in jQuery: moreInfo.hide(); It shows that the behavior your toggling and closing is mor ... read »
Mar 21, 2010 at 3:59 PM
Exploring ColdFusion Component Runtime Class Properties And Serialization
@Elliott, according to Ben's experiment, serializeJSON() doesn't access the private data by default - it doesn't even access the getHair() method - so trying to clone a Girl.cfc via serializeJSON/des ... read »
Mar 21, 2010 at 3:49 PM
Ask Ben: Javascript String Replace Method
I'm confused a bit by what you are asking, but if had this sentence: The color, red, is in the style statement; style: red;. and wanted to remove all or change all of the commas, colons, and semi-c ... read »
Mar 21, 2010 at 3:13 PM
Ask Ben: Javascript String Replace Method
I am trying to make a java program to count the number of times that these punctuation marks occur in a body of text: , : ; . ! - ' " ? / \ I am using this piece to ferret out the commas: numcommas ... read »
Mar 21, 2010 at 11:13 AM
A New Wrist Pain
@chiropractor suwanee, Spoken like someone trying to sell something. Other than for minor, temporary relief from some back pain, chiropractic treatment is nothing but placebo effect and quackery. ... read »
Mar 21, 2010 at 6:32 AM
ColdFusion CFPOP - My First Look
Apologies... The field name in the db for C. is "BounceCode" It stores the code / message which is returned in the email. Sorry for the confusion. ... read »
Mar 21, 2010 at 6:29 AM
ColdFusion CFPOP - My First Look
@Jose Galdamez, Hi Ben and Jose 1st of all.. big thanks to Jose for his Skype chat a few weeks back. Your time was much appreciated. I have come up with a rather unelegant solution to my problem a ... read »