Friday, 31 May 2013

Passing a complex object to a ColdFusion RESTful web service

G'day:
I googled everywhere for how to do this, and drew a complete blank. I had to fall on the mercy of a friendly Adobe engineer to help me get it working, so I thought I would knock together a quick article so if anyone else ever needs to do this, there's something for Google to find.

Firstly, I gotta say that on the whole I probably would not create a RESTful web service which accepts a complex value... it seems to make more sense to me to work with simply types, and perhaps reflect any complex nature using collections of simple-value fields, or JSON or something. Still: I had this specific requirement which was out of my hands, so I needed to make it work.


Just before I get onto the code, I'll dwell on this. I read the docs for "RESTful Web Services in ColdFusion", which say this:

First on the "REST services and data interchange formats" page:
REST services in ColdFusion can be represented in XML or JSON formats for transmitting over the web. ColdFusion takes care of the serialization/deserialization of the ColdFusion data types used in the services to XML or JSON implicitly.

A function that is REST-enabled can take the following ColdFusion data types as arguments: query, struct, CFC type, array, array of CFCs, XML, string, numeric, boolean, date, and binary (for XML).

ColdFusion serializes data to pre-defined formats of XML and JSON. Similarly, ColdFusion deserializes the body only if the body is in the format defined by ColdFusion.
OK, so based on this, I can pass a serialised struct into a method which expects a struct, and it should work. Cool.

Then on the "JSON serialization and REST services" page:

Deserialization specifications

  • The content of the request is in a predefined format specified by ColdFusion.
  • The content type of the request is text/JSON, application/JSON, or text/plain.
  • A function in the service consumes the MIME type of the request.
  • cfargument does not have the attributes restargsource and restargname specified.
  • cfargument type is ColdFusion supported data type other than binary or CFC definition.
  • Only one argument has no restAargSource attribute specified.
  • The whole body of the request is deserialized to the argument type.
  • Cyclic behavior is unsupported. But in the case of cyclic arrays, you might see the deserialized array published, but not giving expected output.
So the key bits here are these:
  • I can pass a struct;
  • I need to pass it as JSON;
  • the function I'm passing it to needs to consume JSON;
  • I don't have any of ColdFusion's special REST parameters on the argument definition;
  • as above, I need to specify the argument as a struct;
  • I need to pass the actual struct in the request body.
OK, and the code:

// Application.cfc
component {

    this.name = "testSerialiser03";

    public void function onApplicationStart(){
        restInitApplication(expandPath("../components"), "components");
    }

}  

// components/TestSerialiser.cfc
component rest=true restPath="testserialiser" {

    remote struct function echoStruct(required struct data  ) httpmethod="post" restpath="echoStruct" consumes="application/JSON" produces="application/JSON" {
        data.uid = createUuid();
        return data;
    }

}

<!--- test.cfm --->
<cfset two = {id=2, en="two", ma="rua"}>
<cfdump var="#two#" label="Original">

<cfhttp method="post" url="http://#CGI.http_host#/rest/components/testserialiser/echoStruct/" result="response">
    <cfhttpparam type="header" name="Content-Type" value="application/JSON">
    <cfhttpparam type="body" value="#serializeJson(two)#">
</cfhttp>
<cfdump var="#response#" label="HTTP response">

<cfset echoed = deserializeJson(response.filecontent)>
<cfdump var="#echoed#" label="Result">

And running this, I get this output:

Original - struct
ENtwo
ID2
MArua
HTTP Response - struct
Charset[empty string]
ErrorDetail[empty string]
Filecontent{"EN":"two","UID":"CDA59632-EE5B-60E4-BF994402F7130EB4","ID":2,"MA":"rua"}
HeaderHTTP/1.1 200 OK Server: Apache-Coyote/1.1 CF_TOMCAT_REUSE_THIS_CONNECTION: FALSE Content-Type: application/JSON Content-Length: 74 Date: Fri, 31 May 2013 14:21:28 GMT Connection: close
Mimetypeapplication/json
Responseheader
HTTP Response - struct
CF_TOMCAT_REUSE_THIS_CONNECTIONFALSE
Connectionclose
Content-Length74
Content-Typeapplication/JSON
DateFri, 31 May 2013 14:21:28 GMT
ExplanationOK
Http_VersionHTTP/1.1
ServerApache-Coyote/1.1
Status_Code200
Statuscode200 OK
TextYES
Result - struct
ENtwo
ID2
MArua
UIDCDA59632-EE5B-60E4-BF994402F7130EB4

So that's a silly demonstration, but it demonstrates the point.

OK, so why did I have to rely on a friendly Adobe engineer to help me out with this... all I needed to do is to follow the instructions, right? Yeah, well: had I followed all the instructions properly, it'd've worked first time.

However I overlooked this bit:

The content type of the request is text/JSON, application/JSON, or text/plain.


<cfhttp method="post" url="http://#CGI.http_host#/rest/components/testserialiser/echoStruct/" result="response">
    <cfhttpparam type="header" name="Content-Type" value="application/JSON">
    <cfhttpparam type="body" value="#serializeJson(two)#">
</cfhttp>

Basically I forgot to put that <cfhttpparam> in. Doh! I tried about one million different things to try to get it to go without that, and it just cacked out, giving me one of these:

HTTP/1.1 415 Unsupported Media Type Server: Apache-Coyote/1.1 Content-Type: application/JSON

or:

Application components could not be initialized.

Reason: Missing dependency for method public java.util.Map testing.rest.components.TestSerialiser.echoStructViaUrl(java.util.Map) throws coldfusion.xml.rpc.CFCInvocationException at parameter at index 0

depending on what I was doing.

So that's that.

Hopefully I've included enough content in here that if someone needs to google this stuff up, they'll find this example.

The upside for me is that now I've read a lot more about ColdFusion RESTful web services, and have practised a lot of different bits of functionality they offer. So that's good!

Righto.

--
Adam