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() 2009 (Minneapolis, MN) with:

Copy / Import XML Nodes Into A ColdFusion XML Document

By Ben Nadel on
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:

  • <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:

  • <!--- 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):

  • <!---
  • 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:

  • <!---
  • 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.




Reader 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

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

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.

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

Has anyone has success using the ToString() function to export the new XML document to a file?

I am running into a problem where I get random "<!---->" inserted into the raw XML. For example:

  • <NOTE Comments="" FillColor="12695295" FontColor="0" ID="8240" LineColor="0" Name="Note_02" Text="\n\n\n" XPos="4020" YPos="200"><bold><!----></bold></NOTE><NOTE Comments="" FillColor="14745599" FontColor="0" ID="8241" LineColor="0" Name="Note_02" Text="\n\n\n" XPos="4020" YPos="290"><bold><!----></bold></NOTE>

I am using MicroOlap Database Designer and these comment strings are keeping the notes and tables I've imported from another document from showing. When I <cfdump> it looks fine, but looking at the raw XML you can see the comment strings. Any thoughts?

Hi Ben,

First of all thanks for this function. It has saved me quite some time in an already time-consuming process.

For Dutch University Repositories using large XML structures I made a few slight alterations to your function, so it will only specify stuff when required, leaving out empty attributes, comments, etc. It just provides cleaner, more compact XML now.

The code is as follows.

  • <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: Created an IF statement to automatically select
  • the right method.
  • --->
  •  
  • <cfif isDefined('ARGUMENTS.Nodes.XmlNsURI') and ARGUMENTS.Nodes.XmlNsURI neq ''>
  • <cfset LOCAL.NewNode = XmlElemNew(
  • ARGUMENTS.ParentDocument,
  • ARGUMENTS.Nodes.XmlNsURI,
  • ARGUMENTS.Nodes.XmlName
  • ) />
  • <cfelse>
  • <cfset LOCAL.NewNode = XmlElemNew(
  • ARGUMENTS.ParentDocument,
  • ARGUMENTS.Nodes.XmlName
  • ) />
  • </cfif>
  •  
  • <!--- Append the XML attributes. --->
  • <cfset StructAppend(
  • LOCAL.NewNode.XmlAttributes,
  • ARGUMENTS.Nodes.XmlAttributes
  • ) />
  •  
  • <!--- Copy simple values (only when needed, to prevent verbose output). --->
  • <cfif ARGUMENTS.Nodes.XmlNsPrefix neq ''>
  • <cfset LOCAL.NewNode.XmlNsPrefix = ARGUMENTS.Nodes.XmlNsPrefix />
  • </cfif>
  • <cfif ARGUMENTS.Nodes.XmlNsUri neq ''>
  • <cfset LOCAL.NewNode.XmlNsUri = ARGUMENTS.Nodes.XmlNsUri />
  • </cfif>
  • <cfif ARGUMENTS.Nodes.XmlText neq ''>
  • <cfset LOCAL.NewNode.XmlText = ARGUMENTS.Nodes.XmlText />
  • </cfif>
  • <cfif ARGUMENTS.Nodes.XmlComment neq ''>
  • <cfset LOCAL.NewNode.XmlComment = ARGUMENTS.Nodes.XmlComment />
  • </cfif>
  •  
  • <!---
  • 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>

Thanks again Ben!

Cheers,

Michiel

I absolutely love this solution, but have a problem I can't seem to resolve: The element I want to insert has both text and children, which are interspersed (example: This is my example.) When the xmlText is copied to the new node, the child is not in the correct sequence - it appears after the xmlText, not in the middle.

When I try to importXML using xmlNodes, then I get an error stating

Invalid XML identifier encountered.

The identifier [#text] is not a valid XML identifier.

Suggestions?