Wednesday, October 15, 2008

Compressing CFXML Objects

Yesterday I talked about creating ColdFusion web services. In that example, I generated the XML using cfscript due to the fact that it generates compressed XML. However, it is also possible to use the cfxml tag. The issue here is that it preserves all the formatting you use in your code between those tags.

If you had some curiosity why it would keep all the formatting, the answer is in the XmlText attribute of the object itself. Anything between the beginning and ending of the tag that is not contained in other tags is included in the tag's text, which by default includes all XML formatting.

Using the cfxml tag is much more friendly and efficient, in general, than using cfscript. However, when the client wants the XML to use as little bandwidth as necessary, how do you get around that?

In order to have the tag compressed, I've developed an XML compression function. It will take any XML object as a parameter, and rebuilds it with script. Doing this, it strips out all the formatting between the tags by testing the XmlText. If that XmlText, when trimmed, provides no information, it is not valid text and is removed from that tag. This effectively removes any white space that was used in formatting, while preserving spacing between words in text for children.

The functions I developed to do this are included below, should you wish to use them. These functions are written in cfscript and must be contained in a cfscript block according.

Note: In ColdFusion 8, all cfscript functions declared in a CFC are public. The function declaration ise done with cffunction, then the body contained in script, to prevent web service users from calling it directly.

The functions are included below. In the event that they are being placed in a CFC to be used internally by ColdFusion apps, the first function can be set to access public. The second function, used only for recursion, should never be made public.

  <cffunction access="private" name="XMLCompress" returntype="xml">
<cfargument name="sourceXML" type="xml" required="yes">
<cfscript>
var toReturn = xmlNew(false);
var newRoot = XMLTagCompress(toReturn, sourceXML.XmlRoot);
toReturn.xmlRoot = xmlElemNew(toReturn, sourceXML.xmlRoot.XmlName);
for (child = 1; child LTE ArrayLen(sourceXML.xmlRoot.XmlChildren); child ++) {
toReturn.xmlRoot.XmlChildren[child] = XmlTagCompress(toReturn, sourceXML.xmlRoot.XmlChildren[child]);
}
return toReturn;
</cfscript>
</cffunction>

<cffunction access="private" name="XMLTagCompress" returntype="xml">
<cfargument name="sourceXML" type="xml" required="yes">
<cfargument name="xmlElement" type="xml" required="yes">
<cfscript>
var currentChild = 1;
var compressedNode = xmlElemNew(sourceXML, xmlElement.XmlName);
if (Len(Trim(xmlElement.XmlText)) GT 0)
compressedNode.XmlText = xmlElement.XmlText;
for (attrib in xmlElement.XmlAttributes) {
compressedNode.XmlAttributes[attrib] = xmlElement.XmlAttributes[attrib];
}
while (currentChild LTE ArrayLen(xmlElement.XmlChildren)) {
compressedNode.XmlChildren[currentChild] = XMLTagCompress(sourceXML, xmlElement.XmlChildren[currentChild]);
currentChild ++;
}
return compressedNode;
</cfscript>
</cffunction>


With these functions, I was able to rewrite yesterday's Coldfusion web service to appear as shown below:

  <cffunction name="getOSInfo"
access="remote"
output="yes">
<cfargument name="os_or"
type="numeric"
required="no"
default="0">
<cfcontent type="application/xml" reset="yes">
<cfquery name="getOS" datasource="[[removed for security]]">
[[ removed for security ]]
</cfquery>
<!--- Tag XML does not reduce white space--->
<cfxml variable="xmlValue">
<oslist>
<cfloop query="getOS">
<os id="#getOS.id#">
<brand>#XMLFormat(getOS.brand)#</brand>
<family>#XMLFormat(getOS.family)#</family>
<type>#XMLFormat(getOS.type)#</type>
<name>#XMLFormat(getOS.name)#</name>
</os>
</cfloop>
</oslist>
</cfxml>
<!--- By running the custom XMLCompress function
the data is returned with formatting removed --->
#XMLCompress(xmlValue)#
<cfabort>
</cffunction>

Tuesday, October 14, 2008

ColdFusion Web Services

So, for the past couple of days I've been tasked with developing web services to interact with our database. This is an idea I've strongly recommended to my team in order to minimize the exposure of our architecture to other developers and consumers.

My current team operates entirely within ColdFusion. After some research, I was able to determine that CFCs can be called with a direct HTTP request using parameters. This was exactly what I needed, however as we've developed them we ran into a small difficulty.

Initially in our design, we would have set the cffunction attribute "returntype" to "xml", and return a cfxml object. This would return the XML object properly formed with an XML header tag at the beginning. This works just fine when dealing with parsing it in PHP, ASP, or another server technology, and can be recognized by some browsers. However, it technically was not complete.

When consumed by a browser, the content-type returned was "text/html". This content-type was not desirable when the only return value was XML. Unfortunately, because this was a function returning an object, and not outputting a page, we were unable to send a Content-Type header of our own choosing under this format. This would cause Firefox to read the content as HTML and would only show the content of tags, not the full XML tree as it would with a proper XML document.

To handle this, we changed function specification. It technically no longer returns anything. The "returntype" parameter has been removed, and the cfreturn tag at the end was deleted. In our new format, we set the "output" parameter to "yes", and essentially build the document inside the function itself. This allows us to use the cfcontent tag and specify the content type we would like to be sent.

Because the output parameter causes the function to build the document as output, we could then control the inside of the function as if it were generating a page on the server. We were able to begin with the cfcontent tag, setting its type to "application/xml" and reset to "yes" in order to prevent it from adding any header information the server may insist on. This also allowed us to use the cfprocessingdirective to suppress white space, decreasing the total byte size transmitted.

We still store everything in a cfxml variable because it properly formats the tags and sets the xml document specification correctly. It is built within that variable. However, with the output trait set and no return value, we have to treat this like a page, meaning that the variable must be output.

The best way to output the variable that I've found is to use the ToString method as shown below:
#ToString(xmlVariable)#
This will prevent a problem with creating a blank row at the top that would cause the document to break in Firefox. It also allows us to build the XML document in a way that is more portable than plain-text should the function specification need to be changed in the future.

Unfortunately, the ToString function will preserve any formatting that existed in creating the variable, so white space suppression within the XML must be done in the actual source function. There is a way around it using ColdFusion XML functions which I may write about at another time.

Below, I've included a sample CFC I created that demonstrates this creation process:


<cfcomponent>
<cffunction name="getOSInfo"
access="remote"
output="yes">
<cfargument name="id"
type="numeric"
required="no"
default="0">
<!--- Set file content header to "application/xml"
Reset may be necessary if the server inserts content
on delivery --->
<cfcontent type="application/xml" reset="yes">
<cfquery name="getOS" datasource="[[removed for security]]">
[[Query removed for security]]
</cfquery>
<!--- Script-based XML suppresses white space --->
<cfscript>
xmlValue = xmlNew(false);
xmlValue.xmlRoot = xmlElemNew(xmlValue, "oslist");
outputString = "";

for (i = 1; i < getOS.recordCount; i ++) {
// Create OS record
xmlValue.oslist.xmlChildren[i] =
xmlElemNew(xmlValue, "os");
xmlValue.oslist.os[i].xmlAttributes["id"] =
getOS.id[i];

// Create brand for OS
xmlValue.oslist.os[i].xmlChildren[1] =
xmlElemNew(xmlValue, "brand");
// Ensures that tag only receives content if
// there is valid content for it
if (getOS.brand[i] NEQ "")
xmlValue.oslist.os[i].brand.XmlText =
XMLFormat(getOS.brand[i]);

// Create family for OS
xmlValue.oslist.os[i].xmlChildren[2] =
xmlElemNew(xmlValue, "family");
if (getOS.family[i] NEQ "")
xmlValue.oslist.os[i].family.XmlText =
XMLFormat(getOS.family[i]);

// Create type for OS
xmlValue.oslist.os[i].xmlChildren[3] =
xmlElemNew(xmlValue, "type");
if (getOS.type[i] NEQ "")
xmlValue.oslist.os[i].type.XmlText =
XMLFormat(getOS.family[i]);

// Create name for OS
xmlValue.oslist.os[i].xmlChildren[4] =
xmlElemNew(xmlValue, "name");
if (getOS.name[i] NEQ "")
xmlValue.oslist.os[i].name.XmlText =
XMLFormat(getOS.name[i]);
}
</cfscript>
<!--- Output XML value directly
rather than returning it --->
#xmlValue#
</cfprocessingdirective>
<!--- Abort may be necessary to prevent
post-processing --->
<cfabort>
</cffunction>
</cfcomponent>