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>

2 comments:

CisAnubhav said...
This comment has been removed by the author.
CisAnubhav said...

A very good blog to understand coldfusion web services. From a long time i have been searching such details but found now.
Thanks