I've been on and off working on SOAP using ColdFusion 9 (CF9) for a while. I found out that the support of SOAP was not fully implemented. For example, when a SOAP exception happens, CF9 throws SOAP fault in the form of regular ColdFusion exception. That's definitely not right! We should deliver the fault in SOAP format. Again, this broken support is happening on CF9. I didn't check on other CF versions.
To fix the situation, I wrote a process to intercept the CF exception, parse the SOAP fault, and deliver it in SOAP format. First, I utilize onError() in Application.cfc to intercept an exception. Second, there is a CFC to parse the fault that is in the exception.
NOTE: My code includes a user-defined function, arrayOfStructsSoft() from Nathan Dintenfass. Thanks, buddy!
Application.cfc
<cfcomponent output="false"> <cfset this.name = "soap" /> <!--- When error occurred, the SOAP throws a ColdFusion exception that contains text-based fault info. That's the reason we need to parse and return it as a SOAP XML. This method generates SOAP envelope that contains fault in the body. ---> <cffunction name="onError" returntype="void"> <cfargument name="exception" required="true" /> <cfargument name="eventName" type="string" required="true" /> <cfset var soap_envelope = "" /> <cfset var soap_fault = createObject("component", "SoapFault").init(arguments.exception['detail']) /> <cfset var fault_string = toString(soap_fault.getFaultNode("xml")) /> <!--- Remove XML header ---> <cfset fault_string = replaceNoCase(fault_string, '<?xml version="1.0" encoding="UTF-8"?>', '') /> <!--- Replace <Fault /> with <soapenv:Fault /> to follow ColdFusion naming conventions ---> <cfset fault_string = replaceNoCase(fault_string, '<Fault>', '<soapenv:Fault>') /> <cfset fault_string = replaceNoCase(fault_string, '</Fault>', '</soapenv:Fault>') /> <!--- Embed the fault to SOAP envelope ---> <cfsavecontent variable="soap_envelope"> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" <!--- SOAP 1.1 ---> xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soapenv:Body> <cfoutput>#fault_string#</cfoutput> </soapenv:Body> </soapenv:Envelope> </cfsavecontent> <!--- Output the result as a SOAP XML. So, the caller can receive and digest an exception/fault in SOAP format instead of ColdFusion native format. ---> <cfcontent type="text/xml; charset=utf-8" /> <cfoutput>#xmlParse(soap_envelope)#</cfoutput> <!--- xmlParse() converts string to XML ---> </cffunction> </cfcomponent>
SoapFault.cfc
<cfcomponent output="false"> <cfset variables.instance = {} /> <cfset variables.instance["fault"] = {} /> <cffunction name="init" output="false"> <cfargument name="exception" type="string" required="false" default="" /> <cfif len(arguments.exception)> <cfset variables.instance["fault"] = parseFault(arguments.exception) /> </cfif> <cfreturn this /> </cffunction> <cffunction name="parseFault" output="false" access="public" returntype="struct"> <cfargument name="exception" type="string" required="true" /> <!--- Receive an exception string from ColdFusion that contains SOAP fault information ---> <!--- REFERENCE: http://axis.apache.org/axis/java/apiDocs/org/apache/axis/AxisFault.html ---> <!--- The order should be matched with the one that ColdFusion outputs ---> <cfset var node_list = "faultCode,faultSubcode,faultString,faultActor,faultNode,faultDetail" /> <cfset var node_pos1 = 0 /> <cfset var node_pos2 = 0 /> <cfset var node_name = "" /> <cfset var node_array[1] = {element_name=node_name, element_position=node_pos1} /> <cfset var fault_info_pos1 = 0 /> <cfset var fault_info = "" /> <cfset var fault_node = "" /> <cfset var result = {} /> <cfset var i = 1 /> <cftry> <!--- Build array that contains the element name and its position ---> <cfloop list="#node_list#" index="node_name"> <!--- Get the start position of each element ---> <cfset node_pos1 = findNoCase(node_name & ":", arguments.exception) /> <!--- Build a list based on what it is found ---> <cfif node_pos1> <!--- Build a struct and set it to array ---> <cfset node_array[i] = {element_name=node_name, element_position=node_pos1} /> <!--- Increment array index ---> <cfset i = i + 1 /> </cfif> </cfloop> <!--- Sort the structure ---> <cfset node_array = arrayOfStructsSort(node_array, "element_position", "asc", "numeric") /> <cfloop index="i" from="1" to="#arrayLen(node_array)#"> <!--- Assume that all node in the list is found. In other words, no element_position is zero. ---> <cfset node_pos1 = findNoCase(node_array[i].element_name, arguments.exception) /> <!--- Assume that faultDetail is in the last list ---> <cfif node_array[i].element_name NEQ "faultDetail"> <!--- Get the end position and use the next element as a reference ---> <cfset node_pos2 = findNoCase(node_array[i+1].element_name, arguments.exception, node_pos1) /> <cfelse> <!--- For the last element, faultDetail, use </pre> as the reference The tag is found in cfcatch.detail ---> <cfset node_pos2 = findNoCase("</pre>", arguments.exception, node_pos1) /> <!--- If cannot find the tag, find "hostname:" "hostname:" is found in cfcatch.faultString ---> <cfif NOT node_pos2> <cfset node_pos2 = findNoCase("hostname:", arguments.exception, node_pos1) /> </cfif> </cfif> <!--- Get the whole string from "elementName:" to the next element ---> <cfset fault_node = mid(arguments.exception, node_pos1, node_pos2 - node_pos1) /> <!--- Get position of the current element value ---> <cfset fault_info_pos1 = find(":", fault_node) + 1 /> <!--- Get the element value ---> <cfset fault_info = trim( mid(fault_node, fault_info_pos1, len(fault_node) - fault_info_pos1 + 1) ) /> <!--- Insert in to a struct ---> <cfset result[node_array[i].element_name] = fault_info /> </cfloop> <cfcatch type="any"> <cfloop list="#node_list#" index="node_name"> <cfif node_name EQ "faultString"> <cfset result[node_name] = cfcatch.message /> <cfelseif node_name EQ "faultDetail"> <cfset result[node_name] = cfcatch.detail /> <cfelse> <cfset result[node_name] = "" /> </cfif> </cfloop> </cfcatch> </cftry> <cfreturn result /> </cffunction> <cffunction name="getFaultNode" output="false" returntype="any"> <cfargument name="return_type" type="string" required="false" default="struct" /> <cfset var result = "" /> <cfset var root = "" /> <cfif arguments.return_type EQ "struct"> <cfset result = duplicate(variables.instance["fault"]) /> <cfelseif arguments.return_type EQ "xml"> <cfset result = xmlNew() /> <!--- Create a root element ---> <cfset result.xmlRoot = xmlElemNew(result, "Fault") /> <cfset root = result.xmlRoot /> <!--- For each element, create and populate the element ---> <cfloop collection="#variables.instance["fault"]#" item="element"> <!--- According to http://schemas.xmlsoap.org/soap/envelope/, the element name should be all lower case ---> <cfset arrayAppend(root.xmlChildren, xmlElemNew(result, lCase(element))) /> <cfset root[element].xmlText = xmlFormat(variables.instance["fault"][element]) /> </cfloop> </cfif> <cfreturn result /> </cffunction> <cffunction name="getFaultCode" output="false" returntype="string"> <cfreturn variables.instance["fault"]["faultCode"] /> </cffunction> <cffunction name="getFaultString" output="false" returntype="string"> <cfreturn variables.instance["fault"]["FaultString"] /> </cffunction> <cffunction name="getFaultActor" output="false" returntype="string"> <cfreturn variables.instance["fault"]["faultActor"] /> </cffunction> <cffunction name="getFaultDetail" output="false" returntype="string"> <cfreturn variables.instance["fault"]["faultDetail"] /> </cffunction> <cfscript> /** * Sorts an array of structures based on a key in the structures. * * @param aofS Array of structures. (Required) * @param key Key to sort by. (Required) * @param sortOrder Order to sort by, asc or desc. (Optional) * @param sortType Text, textnocase, or numeric. (Optional) * @param delim Delimiter used for temporary data storage. Must not exist in data. Defaults to a period. (Optional) * @return Returns a sorted array. * @author Nathan Dintenfass (nathan@changemedia.com) * @version 1, April 4, 2013 */ private function arrayOfStructsSort(aOfS,key){ //by default we'll use an ascending sort var sortOrder = "asc"; //by default, we'll use a textnocase sort var sortType = "textnocase"; //by default, use ascii character 30 as the delim var delim = "."; //make an array to hold the sort stuff var sortArray = arraynew(1); //make an array to return var returnArray = arraynew(1); //grab the number of elements in the array (used in the loops) var count = arrayLen(aOfS); //make a variable to use in the loop var ii = 1; //if there is a 3rd argument, set the sortOrder if(arraylen(arguments) GT 2) sortOrder = arguments[3]; //if there is a 4th argument, set the sortType if(arraylen(arguments) GT 3) sortType = arguments[4]; //if there is a 5th argument, set the delim if(arraylen(arguments) GT 4) delim = arguments[5]; //loop over the array of structs, building the sortArray for(ii = 1; ii lte count; ii = ii + 1) sortArray[ii] = aOfS[ii][key] & delim & ii; //now sort the array arraySort(sortArray,sortType,sortOrder); //now build the return array for(ii = 1; ii lte count; ii = ii + 1) returnArray[ii] = aOfS[listLast(sortArray[ii],delim)]; //return the array return returnArray; } </cfscript> </cfcomponent>
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.