Learning ColdFusion 9: Using OnCFCRequest() To Return JSONP (JSON With Padding) API Responses

Posted July 20, 2009 at 8:50 AM

Tags: ColdFusion

A few days ago, I blogged about the new OnCFCRequest() event handler in ColdFusion 9's Application.cfc framework. This event handler controls requests made to ColdFusion components as opposed to the OnRequest() event handler which, in ColdFusion 9, only handles requests made to ColdFusion templates (CFM file). Over the weekend, I was thinking more about this new method and I thought of a great use case for it: returning JSONP requests. JSONP stands for "JSON With Padding" and is a technique in which JSON data is wrapped in a Javascript method call before it is returned to the client. You can read more about this technique elsewhere, but it is essentially a way to perform GET-based cross-site AJAX requests.

 
 
 
 
 
 
 
 
 
 

JSON and JSONP return different types of data. JSON returns a mime type of "text/x-json" whereas a JSONP request returns a mime type of "text/javascript". So, for example, if a regular JSON response looked like this:

{"name":"Tricia"}

... which is plain text, a JSONP request with the callback "doSomething", would look like this:

doSomething( {"name":"Tricia"} );

Notice here that rather than returning a JSON response, we are actually returning a line of executable Javascript code (hence the mime type text/javascript).

ColdFusion 9's new OnCFCRequest() event handler is the perfect way to serve up JSONP responses because it proxies the actual CFC request. Therefore, it can check to see if the given request has a "callback" parameter; and, if it does, it can transform its return JSON data into a JSONP Javascript execution. To demonstrate this, I am going to take the Application.cfc from my previous OnCFCRequest() demo, pair it down, and then add a bit of response logic:

Application.cfc

 Launch code in new window » Download code as text file »

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application settings. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 2, 0 ) />
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!---
  • Create a cache for our API objects. Each object will
  • be cached at it's name/path such that each remote
  • method call does NOT have to mean a new ColdFusion
  • component instantiation.
  • --->
  • <cfset application.apiCache = {} />
  •  
  • <!--- Return true so page can run. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the request for both CFM and CFC requests.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="template"
  • type="string"
  • required="true"
  • hint="I am the template requested by the user."
  • />
  •  
  • <!--- Set page request settings. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Check for manual application reset. --->
  • <cfif structKeyExists( url, "reset" )>
  • <cfset this.onApplicationStart() />
  • </cfif>
  •  
  • <!--- Return true so page can run. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFM request..">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="template"
  • type="string"
  • required="true"
  • hint="I am the template requested by the user."
  • />
  •  
  • <!--- Include the requested page. --->
  • <cfinclude template="#arguments.template#" />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onCFCRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I process the user's CFC request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="component"
  • type="string"
  • required="true"
  • hint="I am the component requested by the user."
  • />
  •  
  • <cfargument
  • name="methodName"
  • type="string"
  • required="true"
  • hint="I am the method requested by the user."
  • />
  •  
  • <cfargument
  • name="methodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection sent by the user."
  • />
  •  
  • <!---
  • Check to see if the target CFC exists in our cache.
  • If it doesn't then, create it and cached it.
  • --->
  • <cfif !structKeyExists( application.apiCache, arguments.component )>
  •  
  • <!---
  • Create the CFC and cache it via its path in the
  • application cache. This way, it will exist for
  • the life of the application.
  • --->
  • <cfset application.apiCache[ arguments.component ] =
  • createObject(
  • "component",
  • arguments.component
  • ).init()
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: At this point, we know that the target
  • component has been created and cached in the
  • application.
  • --->
  •  
  •  
  • <!--- Get the target component out of the cache. --->
  • <cfset local.cfc = application.apiCache[ arguments.component ] />
  •  
  • <!---
  • Execute the remote method call and store the response
  • (note that if the response is void, it will destroy
  • the return variable).
  • --->
  • <cfinvoke
  • returnvariable="local.result"
  • component="#local.cfc#"
  • method="#arguments.methodName#"
  • argumentcollection="#arguments.methodArguments#"
  • />
  •  
  • <!---
  • Create a default response data variable and mime-type.
  • While all the values returned will be string, the
  • string might represent different data structures.
  • --->
  • <cfset local.responseData = "" />
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • <!---
  • Check to see if the method call above resulted in any
  • return value. If it didn't, then we can just use the
  • default response value and mime type.
  • --->
  • <cfif structKeyExists( local, "result" )>
  •  
  • <!---
  • Check to see what kind of return format we need to
  • use in our transformation. Keep in mind that the
  • URL-based return format takes precedence. As such,
  • we're actually going to PARAM the URL-based format
  • with the default in the function. This will make
  • our logic much easier to follow.
  •  
  • NOTE: This expects the returnFormat to be defined
  • on your CFC - a "best practice" with remote
  • method definitions.
  • --->
  • <!--- Get target method return format. --->
  • <cfparam
  • name="url.returnFormat"
  • type="string"
  • default="#getMetaData( local.cfc[ arguments.methodName ] ).returnFormat#"
  • />
  •  
  • <!---
  • Now that we know the URL scope will have the
  • correct format, we can check that exclusively.
  •  
  • NOTE: When checking for JSON responses, check to
  • see if there is a callback. If there is, then we
  • are executing a JSONP (JSON with Padding) response
  • rather than an actual JSON response.
  • --->
  • <cfif (
  • (url.returnFormat eq "json") &&
  • !structKeyExists( url, "callback" )
  • )>
  •  
  • <!--- Convert the result to json. --->
  • <cfset local.responseData = serializeJSON( local.result ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/x-json" />
  •  
  • <cfelseif (
  • (url.returnFormat eq "json") &&
  • structKeyExists( url, "callback" )
  • )>
  •  
  • <!---
  • This is a JSONP (JSON with Padding) resposne.
  • We need to wrap the JSON response in the
  • provided callback execution:
  •  
  • callback( JSON_REPONSE );
  • --->
  • <cfset local.responseData = (
  • "#url.callback#(" &
  • serializeJSON( local.result ) &
  • ");"
  • ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/javascript" />
  •  
  • <cfelseif (url.returnFormat eq "wddx")>
  •  
  • <!--- Convert the result to XML. --->
  • <cfwddx
  • action="cfml2wddx"
  • input="#local.result#"
  • output="local.responseData"
  • />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/xml" />
  •  
  • <cfelse>
  •  
  • <!--- Convert the result to string. --->
  • <cfset local.responseData = local.result />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/plain" />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have our response data and mime type
  • variables defined, we can stream the response back
  • to the client.
  • --->
  •  
  • <!--- Convert the response to binary. --->
  • <cfset local.binaryResponse = toBinary(
  • toBase64( local.responseData )
  • ) />
  •  
  • <!---
  • Set the content length (to help the client know how
  • much data is coming back).
  • --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( local.binaryResponse )#"
  • />
  •  
  • <!--- Stream the content. --->
  • <cfcontent
  • type="#local.responseMimeType#"
  • variable="#local.binaryResponse#"
  • />
  •  
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

Most of this is from my previous post; but, if you look at my CFIF/CFELSEIF statements, you will notice this new logic:

 Launch code in new window » Download code as text file »

  • <!---
  • Now that we know the URL scope will have the
  • correct format, we can check that exclusively.
  •  
  • NOTE: When checking for JSON responses, check to
  • see if there is a callback. If there is, then we
  • are executing a JSONP (JSON with Padding) response
  • rather than an actual JSON response.
  • --->
  • <cfif (
  • (url.returnFormat eq "json") &&
  • !structKeyExists( url, "callback" )
  • )>
  •  
  • <!--- Convert the result to json. --->
  • <cfset local.responseData = serializeJSON( local.result ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/x-json" />
  •  
  • <cfelseif (
  • (url.returnFormat eq "json") &&
  • structKeyExists( url, "callback" )
  • )>
  •  
  • <!---
  • This is a JSONP (JSON with Padding) resposne.
  • We need to wrap the JSON response in the
  • provided callback execution:
  •  
  • callback( JSON_REPONSE );
  • --->
  • <cfset local.responseData = (
  • "#url.callback#(" &
  • serializeJSON( local.result ) &
  • ");"
  • ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/javascript" />
  •  
  •  
  • <!--- .... Other response cases here .... --->
  •  
  • </cfif>

Notice that if we have a JSON request, the OnCFCRequest() logic checks to see if there is a callback parameter in the URL scope. If there is a callback parameter, rather than returning JSON data, it wraps the JSON data in a Javascript method execution using the name of the given callback and returns it as "text/javascript" data.

To test this, I set up a small page that makes two AJAX requests to the remote Service.cfc (previously discussed here). This page makes one requesting for a standard JSON response and one requesting for a JSONP response:

 Launch code in new window » Download code as text file »

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>ColdFusion 9 OnCFCRequest() JSONP Demo</title>
  • <script type="text/javascript" src="jquery-1.3.2.min.js"></script>
  • <script type="text/javascript">
  •  
  • $(function(){
  •  
  • // Make a regular JSON call.
  • $.ajax({
  • type: "get",
  • url: "./Service.cfc",
  • data: {
  • method: "getGirl",
  • name: "Tricia",
  • returnFormat: "json"
  • },
  • dataType: "json",
  • success: function( response ){
  • console.log( "JSON Response:" );
  • console.log( response );
  • }
  • });
  •  
  • // Make a regular JSON with PADDING call.
  • $.ajax({
  • type: "get",
  • url: "./Service.cfc",
  • data: {
  • method: "getGirl",
  • name: "Tricia",
  • returnFormat: "json"
  • },
  • dataType: "jsonp",
  • success: function( response ){
  • console.log( "JSONP Response:" );
  • console.log( response );
  • }
  • });
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  • <!--- Nothing to show here. --->
  • </body>
  • </html>

Notice that the second jQuery-powered AJAX response defines its dataType as "jsonp." In doing so, jQuery knows to define a temporary callback method and pass it along with the AJAX request. When we run the above code, we get the following responses:

{"NAME":"Tricia"}

... and:

jsonp1248093721267({"NAME":"Tricia"});

As you can see, the second request returned an actual line of executable Javascript code using the callback method that jQuery passed in along with the API request.

In ColdFusion 8, if you wanted to return a JSONP response in your CFC-based API, you would have to check for it from within the remote method call and make sure that your ReturnFormat was "plain" and not "json". I think we can all agree that this is a bit of hack and requires the remote method to take on more responsibility that it was designed to. Now, with ColdFusion 9's OnCFCRequest(), we can make JSONP part of the API request/response framework, leaving the individual API objects with the sole responsibility of returning their natural data. The more I think about it, the more I am liking the OnCFCRequest() event handler.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Other Searches  |  Print Page


You Might Also Be Interested In:




Reader Comments

Jul 20, 2009 at 11:02 AM // reply »
40 Comments

Ben,

That is some pretty nice stuff right there! Always a bit trick trying to figure out the design pattern for ajax vs. non-ajax requests.

I especially like that the cfc doesn't have to know anything about what the externals of the app. Just return the data, and let something else do all the work.

Pretty awesome.


Jul 20, 2009 at 11:07 AM // reply »
7,572 Comments

@Brandon,

As far as the AJAX vs. non-AJAX requests, I think JSONP might make a great argument for CFM-based AJAX calls rather than CFC based AJAX calls. Perhaps I'll touch upon that in a blog post.


Jul 20, 2009 at 10:53 PM // reply »
10 Comments

ColdFusion 9 isn't even out yet and you are already demoing framework enhancements. Nice job!


Jul 21, 2009 at 7:42 AM // reply »
7,572 Comments

@Drew,

Thanks Drew, I'm pretty excited about all this stuff.


Post Comment  |  Ask Ben

Recent Blog Comments
Mar 21, 2010 at 11:13 AM
A New Wrist Pain
@chiropractor suwanee, Spoken like someone trying to sell something. Other than for minor, temporary relief from some back pain, chiropractic treatment is nothing but placebo effect and quackery. ... read »
Mar 21, 2010 at 6:32 AM
ColdFusion CFPOP - My First Look
Apologies... The field name in the db for C. is "BounceCode" It stores the code / message which is returned in the email. Sorry for the confusion. ... read »
Mar 21, 2010 at 6:29 AM
ColdFusion CFPOP - My First Look
@Jose Galdamez, Hi Ben and Jose 1st of all.. big thanks to Jose for his Skype chat a few weeks back. Your time was much appreciated. I have come up with a rather unelegant solution to my problem a ... read »
Mar 21, 2010 at 3:42 AM
A New Wrist Pain
Chiropractic treatment is one of the best methods for treating numerous health problems naturally. After years of experience being a chiropractor, I have found that it is a powerful way to solve many ... read »
Mar 20, 2010 at 12:07 PM
Drawing On The iPhone Canvas With jQuery And ColdFusion
Simply awesome. Saved my day. ... read »
Mar 20, 2010 at 9:00 AM
Building A Fixed-Position Bottom Menu Bar (ala FaceBook)
I would like to say thx for an easy way to create a bottom bar. I do have a ?. Is it possible to center the bar if i want to resize it to ex 85%. Regards Offenbach ... read »
Mar 19, 2010 at 7:26 PM
MySQL 3/4 - com.mysql.jdbc.Driver And allowMultiQueries=true
Thank you very much for this post. Adding allowMultiQueries="true" in context.xml didn't help until I added it to url as allowMultiQueries=true Good idea is to use prepared statements and it will he ... read »
Jim
Mar 19, 2010 at 4:49 PM
Nobody Puts Baby In The Corner!
Wow. This is like suddenly finding a support group for your secret shame. I'm not alone! I always liked this movie, even though it is extremely cheesy. I just wish Jennifer Grey hadn't gotten the ... read »