Copy / Import XML Nodes Into A ColdFusion XML Document

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:

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

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?

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.

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.

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?

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

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.