<<

Data Handling

XML

By Dave Ferguson (Bio)
gist

Dealing with XML in ColdFusion is fairly straightforward. ColdFusion provides many built-in functions for doing all sorts of XML based operations. Some of those functions provide streamlined access to create, read, and search an xml document. You can also validate XML data against a DTD or schema and transform XML using XSLT.

Reading an XML Document

Reading in an existing XML document can be done a few ways. For example, you can read the file then process it as XML, or you can have the XML parser read the file.

This example reads the file in and then passes it to the parser to create an XML object:

<cffile action="read" file="#ExpandPath("./order.xml")#" variable="myxml" /> 
<cfset mydoc = XmlParse(myxml) /> 

Alternatively, you can just have the xmlparse function read in the file, thus skipping a step:

<cfset mydoc = XmlParse(ExpandPath("./order.xml")) /> 

The xmlparse function takes the XML document and converts the XML to an object. This now allows you to read the XML as a structure, or depending on XML complexity, an array of structs. You can use cfdump on the result of the xmlparse to see the object.

<cfdump var="#mydoc#" /> 

Dumping the XML will display a short version of the XML. You can click on the dump where it says "[short version]" and it will expand to a long version of the XML. The long version shows how ColdFusion is referencing the XML.

Creating an XML Document

Creating XML in ColdFusion is just as easy as generating HTML output. The cfxml tag is used to generate the XML output:

<!---Get some data from, in this case, the database--->
<cfquery name="getData" datasource="cfartgallery" >
    select ARTISTID, ARTID, ARTNAME, DESCRIPTION, ISSOLD, PRICE, LARGEIMAGE from ART
    where artistid = 1
</cfquery>

<!---Process the query result and generate xml--->
<cfxml variable="artxml"> 
<art artistid="<cfoutput>#getdata.artistid#</cfoutput>"> 
    <cfoutput query="getData"> 
        <piece id="#getData.artid#" available="#getdata.isSOLD#">
            <artname>#getData.artname#</artname>
            <description>#getData.artNAME#</description>
            <image>#getData.LARGEIMAGE#</image>     
            <price>#getData.PRICE#</price>  
        </piece>
    </cfoutput>
</art>
</cfxml>

Write XML Out to a File

<cffile action="write" file="#expandpath('./out.xml')#" output="#artxml#" />

Dump Output to the Browser

<cfdump var="#artxml#" />

Parsing XML

ColdFusion provides a single, yet powerful, function to parse XML. This function can read a file, URL, or a string containing XML. It can also validate the XML based on a DTD or schema.

Basic Parse using an XML Document

<cfset mydoc = xmlParse(ExpandPath("./order.xml", true)) />

When parsing the XML, you can have the parsing function maintain case of the XML nodes or ignore case altogether. If it is necessary to maintain case, then referencing the XML content will change slightly. Assuming that the above XML parse (with case sensitivity turned on) returned the XML below, let's go over some of the nuances.

<myArt>
    <art>
        <itemName Type="1">art name</item>
    </art>
</myArt>  

In the above example, the root node is "myArt". Typically you can reference the root like this:

mydoc.myart

But with case sensitivity on this will cause an error, even if the root node is lowercase. The correct way to reference it:

mydoc.xmlRoot

The same is also true with nodes and attributes. With case sensitivity off, this is perfectly valid:

mydoc.myart.art[1].itemname.xmltext

But with case sensitivity on, the same is accomplished using associative array (bracket) notation.

mydoc.XMLRoot.xmlChildren[1]["itemName"]

The bracket syntax is not exclusive to case sensitivity. You can use it even if case sensitivity is off.

XML Datatype and Strings

At its core, XML is just a fancy string. The xmlparse function takes the string and converts it to something useful. The XML string then becomes an XML datatype in ColdFusion. This datatype is an array of structures and arrays. When saving XML to a document you need to save the XML as a string, not as the XML datatype. Converting the XML to a string is as simple as parsing the XML to begin with; all it takes is a single function call.

<cfset xmlString = toString(myDoc) />

Looping over XML

Looping over data in ColdFusion is not very complicated; however, looping over XML can be a little troublesome. This is due to nesting of nodes, attributes, and case sensitivity. Below are two examples of looping over XML. The XML source for this was the same generated XML from the create XML example above.

The first example takes into account that we know the XML structure. This makes it fairly easy to deal with as we can directly reference the structures in the code:

<cfloop array="#artxml.art.xmlchildren#" index="i" >
    <cfoutput>
        name: #i.artname.xmlText#<br/>
        price: #i.price.xmlText#<br/>
        Available? #yesNoFormat(i.xmlAttributes["available"])#
    </cfoutput>
    <br />
</cfloop>

The above example loops over the xmlChildren of the art node in the XML. By doing it this way, we can treat all the children as an array. This then allows us to isolate each node and reference each item in the node. We can also reference the attributes and output them as well.

However, what if we didn't know the XML structure or wanted to process it as if we didn't? Using cfdump would allow us to see the structure and write code accordingly, but that is not always an option. Also, what if you want to do something with the XML node names and/or content? The example below is written as though nothing is known about the XML structure. While it is not a complete example, you should get the idea.

<cfloop array="#artxml.xmlRoot.xmlchildren#" index="i" >
    <cfoutput>
        <cfif structKeyExists(i, "xmlattributes")>
            <cfloop collection="#i.xmlattributes#" item="a" >
                #a#: #i.xmlAttributes[a]#<br/>
            </cfloop>
        </cfif>
        <cfloop array="#i.xmlchildren#" index="x">
        #x.xmlName#: #x.xmlText#<br/>
        </cfloop>
    </cfoutput>
    <br />
</cfloop>

This example uses the underlying XML structure to get attributes, node names, and note text. It works just like the previous example, but it outputs all nodes for each item based on their node name. This is done by looking at each node and treating its children as an array. It also checks the root node for any attributes and outputs them as necessary.

Setting XML Node Value

Setting the value of a node is just about as easy as outputting it. Taking the example XML created previously as a base, let's update the prices.

<cfloop array="#artxml.art.xmlchildren#" index="i">
    <!---price increase by 20% --->
    <cfset i.price.xmlText = i.price.xmlText*.2 />
</cfloop>

Setting an attribute works about the same. However, we look at the attributes of the node using an associative array and change the available value instead.

<cfloop array="#artxml.art.xmlchildren#" index="i" >
    <!--- make all items available --->
    <cfset i.xmlAttributes['available'] = 1 />
</cfloop>

Convert XML to Query Result Set

Sometimes dealing with large XML files can be cumbersome. Depending on the content of the XML, you can convert it to a query result type structure that will make it a little easier to reference. This is done by using a combination of query, XML, and array functions. Using the previously generated XML, we will take it and convert it to a query:

artQuery = QueryNew("artistid, artid, name, description, image, price, sold"); 
queryAddRow(artQuery, arraylen(myDoc.art.piece));

for (i = 1; i lte arraylen(myDoc.art.piece); i++){
    thisItem = mydoc.art.piece[i];
    QuerySetCell(artQuery, "artistid", mydoc.art.xmlAttributes.artistid, i);
    QuerySetCell(artQuery, "artid", thisItem.xmlAttributes.id, i);
    QuerySetCell(artQuery, "sold", thisItem.xmlAttributes.available, i);
    QuerySetCell(artQuery, "name", thisItem.artname.xmltext, i);
    QuerySetCell(artQuery, "description", thisItem.description.xmltext, i);
    QuerySetCell(artQuery, "image", thisItem.image.xmltext, i);
    QuerySetCell(artQuery, "price", thisItem.price.xmltext, i);

}

writedump(artQuery);

The above example will dump out a query object. You can then use the same query object in a cfoutput or in a cfloop.

Searching XML

ColdFusion 10 supports XPath 2.0 for XMLSearch() and XMLTransform(). This allows for using parameters when searching XML so you can build complicated searches with ease. The example below is just one of many ways to search XML. In the example below, the first step is to load and parse the XML (using XML created above). Then a param is created for the search. Next, the search is performed using the param. The search is looking for a node value of "Mary", where the value resides in the piece/artname path. The "//" before piece means that the node piece can have any parent. The result of the search is an array of results; also, the result is just the node found. Therefore, in the dump, the return var must be treated as an array. It is also necessary to get the parent of the found node so we get the entire art node for the item.

<cffile action="read" file="#expandPath('./out.xml')#" variable="myxml" />

<cfset mydoc = XmlParse(myxml) />
<cfscript> 
    params = structNew(); 
    params["artname"] = "Mary"; 
</cfscript> 

<cfset searchRes = xmlSearch(myxml, '//piece/artname[. = $artname]', params) />

<cfdump var="#searchRes[1].xmlParent#" />