Search This Blog

Saturday, February 22, 2014

ColdFusion: Half Baked SOAP Support in ColdFusion 9

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.