Using An ArgumentCollection URL Parameter With ColdFusion Web Services
The other day, Ray Camden and Stephen Duncan Jr. blew my mind when they demonstrated that you could use the argumentCollection parameter when invoking a ColdFusion web service. I don't mean to be redundant in my blogging; but, this finding is just so ridiculously awesome that I had to spend some time exploring it for myself. Since they spent time looking at it from an AJAX standpoint, however, I'll spend some time looking at it from the server side.
The argumentCollection parameter, in ColdFusion, is a parameter that can be used to collect function arguments before the given function is invoked. This works for both named arguments and ordered arguments - although ordered argument behavior is very contextual. What Ray Camden demonstrated in his blog post was this argumentCollection behavior works with ColdFusion component web services in much the same way that it works with local method invocation.
To demonstrate this, I have created a ColdFusion component, Greeting.cfc, which has a single remote method, sayHello(). This sayHello() method takes a name and an optional compliment and returns a greeting string:
Greeting.cfc
<cfcomponent
output="false"
hint="I provide a simple greeting method.">
<cffunction
name="sayHello"
access="remote"
returntype="string"
output="false"
hint="I say something to the given person.">
<!--- Define arguments. --->
<cfargument
name="name"
type="string"
required="true"
hint="I am the person we are talking to."
/>
<cfargument
name="compliment"
type="string"
required="false"
hint="I am the optional compliment."
/>
<!--- Check to see if the compliment exists. --->
<cfif structKeyExists( arguments, "compliment" )>
<!--- Return just the hello with a compliment. --->
<cfreturn "Hello, #arguments.name#, you are #arguments.compliment#." />
<cfelse>
<!--- Return just the hello. --->
<cfreturn "Hello, #arguments.name#." />
</cfif>
</cffunction>
</cfcomponent>
Notice that the access on the sayHello() method is "remote." This is what allows the method to be invoked as a web service.
Now, to test the use of argumentCollection with this remote method, I created a test page that invokes the given component using CFHTTP:
<!--- Build the URL for the web service. --->
<cfset webServiceUrl = (
"http://" &
cgi.server_name &
getDirectoryFromPath( cgi.script_name ) &
"Greeting.cfc"
) />
<!---
Build the arguments collection that we are going to use to
invoke the Greeting.cfc.
--->
<cfset webServiceArguments = {
name = "Katie",
compliment = "looking super adorable"
} />
<!--- Invoke the web service. --->
<cfhttp
result="get"
method="get"
url="#webServiceUrl#">
<!--- The method we are invoking. --->
<cfhttpparam
type="url"
name="method"
value="sayHello"
/>
<!---
In order to use the argumentCollection approach, we have
to serialize the arguments struct into something that can
be URL-encoded.
--->
<cfhttpparam
type="url"
name="argumentCollection"
value="#serializeJSON( webServiceArguments )#"
/>
<!--- Define the response format (unrelated to demo). --->
<cfhttpparam
type="url"
name="returnFormat"
value="json"
/>
</cfhttp>
<!--- Output the resultant file content. --->
<cfoutput>
#deserializeJSON( get.fileContent )#
</cfoutput>
As you can see here, I am gathering my method arguments in a ColdFusion struct called webServiceArguments. If we were only invoking methods locally, I could simply pass this struct, as is, to the target method as the argumentCollection parameter. However, since we are invoking a remote access component, we have to serialize the argumentCollection parameter when passing it to the CFHTTPParam tag. When parent CFHTTP tag executes and returns the result, we get the following output:
Hello, Katie, you are looking super adorable.
How sweet-ass-sweet is that?!? We passed a serialized argumentCollection parameter over the URL and it worked perfectly with the remote access method invocation.
NOTE: This works with both GET and POST. But, for our demos, we will stick with GET.
As of ColdFusion 8, there are now two serialization formats natively supported: JSON and WDDX. The above demo used serializeJSON() to serialize the argumentCollection using the JSON format. But, would this same technique work if we used the CFWDDX tag and the WDDX open standard?
To test this, I augmented the previous demo to serialize the webServiceArguments using the CFWDDX before executing the CFHTTP tag:
<!--- Build the URL for the web service. --->
<cfset webServiceUrl = (
"http://" &
cgi.server_name &
getDirectoryFromPath( cgi.script_name ) &
"Greeting.cfc"
) />
<!---
Build the arguments collection that we are going to use to
invoke the Greeting.cfc.
--->
<cfset webServiceArguments = {
name = "Katie",
compliment = "looking super adorable"
} />
<!---
Serialize the argument collection using CFWDDX and the
WDDX open standard.
--->
<cfwddx
output="wddxArguments"
action="cfml2wddx"
input="#webServiceArguments#"
/>
<!--- Invoke the web service. --->
<cfhttp
result="get"
method="get"
url="#webServiceUrl#">
<!--- The method we are invoking. --->
<cfhttpparam
type="url"
name="method"
value="sayHello"
/>
<!---
In order to use the argumentCollection approach, we have
to serialize the arguments struct into something that can
be URL-encoded.
--->
<cfhttpparam
type="url"
name="argumentCollection"
value="#wddxArguments#"
/>
<!--- Define the response format (unrelated to demo). --->
<cfhttpparam
type="url"
name="returnFormat"
value="json"
/>
</cfhttp>
<!--- Output the resultant file content. --->
<cfoutput>
#deserializeJSON( get.fileContent )#
</cfoutput>
Notice that the serialization of our argumentCollection has nothing to do with the returnFormat that we want the remote ColdFusion component to use. In this case, we are sending the argumentCollection parameter as WDDX format but we are asking for a response in JSON format. And, when we run the above code this time, we get the following output:
Hello, Katie, you are looking super adorable.
Awesome! As long as we use a serialization format that ColdFusion understands, it doesn't much matter which serialization approach we use. But, what is ColdFusion actually seeing on the invocation end of this communication? Where is the auto-magical deserialization happening?
To explore this aspect more thoroughly, I set up an Application.cfc ColdFusion framework component that would allow me to see what URL parameters were being passed to the remote access component.
NOTE: Before I did this, I reverted the calling code to using the JSON format.
Application.cfc
<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, 5, 0 ) />
<!--- Define the log file path. --->
<cfset this.logFilePath = (
getDirectoryFromPath( getCurrentTemplatePath() ) &
"log.htm"
) />
<cffunction
name="onRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the request.">
<!--- Log the incoming URL scope. --->
<cfdump
var="#url#"
output="#this.logFilePath#"
label="URL - #getFileFromPath( cgi.script_name )#"
format="html"
/>
<!--- Return true so the page can process. --->
<cfreturn true />
</cffunction>
</cfcomponent>
As you can see, this Application.cfc component does nothing more than log the incoming URL scope to a log file. And, when we invoke our remote access component using our first demo (the JSON version), we get the following data CFDump'd to our log file:
This is very interesting. I would have expected the argumentCollection URL parameter to have been magically deserialized at this point; however, as you can see, it is still a serialized JSON string.
Now, this got me thinking - can you use a serialized argumentCollection string to invoke local methods as well? As a quick sanity check, I tried this very approach (abbreviated demo):
<cfset message = createObject( "component", "Greeting" ).sayHello(
argumentCollection = serializeJSON( webServiceArguments )
) />
Running this code, however, results in the following ColdFusion error:
Error casting an object of type java.lang.String cannot be cast to java.util.Map to an incompatible type. java.lang.String cannot be cast to java.util.Map.
Ok, so ultimately, a ColdFusion function or method still needs to be invoked with an object that upholds the Java Map interface. This means that in our serialized argumentCollection demo, there is some magical conversion that takes place behind the scenes.
While I understand that ColdFusion is trying not to make too many assumptions about how URL parameters are going to be used, the fact that the argumentCollection remains deserialized does complicate interception. If we wanted to step in and take over the processing work flow, manually invoking the local component as part of the remote invocation, the argumentCollection in serialized format will throw an error.
This got me thinking - what happens if I deserialize the argumentCollection from within the onRequestStart() event handler. If this worked, then I could create an environment in which both automatic and manual invocation could be (more) successful. To test this, I augmented the onRequestStart() event handler to deserialize the JSON:
Application.cfc - Manual Deserialization
<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, 5, 0 ) />
<!--- Define the log file path. --->
<cfset this.logFilePath = (
getDirectoryFromPath( getCurrentTemplatePath() ) &
"log.htm"
) />
<cffunction
name="onRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the request.">
<!---
Check to see if we have a serailized argument
collection that we can deserialize in order to
normalize the request.
--->
<cfif (
structKeyExists( url, "argumentCollection" ) &&
isSimpleValue( url.argumentCollection )
)>
<!---
Deserialize the collection and append it to the
incoming URL scope.
--->
<cfset url.argumentCollection = deserializeJSON(
url.argumentCollection
) />
</cfif>
<!--- Log the incoming URL scope. --->
<cfdump
var="#url#"
output="#this.logFilePath#"
label="URL - #getFileFromPath( cgi.script_name )#"
format="html"
/>
<!--- Return true so the page can process. --->
<cfreturn true />
</cffunction>
</cfcomponent>
Here, we are attempting to normalize the incoming arguments in a way that will work with both automatic and manual invocation; however, when we try this approach, we get the following ColdFusion error:
Error casting an object of type coldfusion.runtime.Struct cannot be cast to java.lang.String to an incompatible type. coldfusion.runtime.Struct cannot be cast to java.lang.String.
It looks like an HTTP-based argumentCollection parameter needs to be serialized. As such, we can't normalize the request by altering the argumentCollection alone. But, what if we get rid of the argumentCollection altogether? What if, rather than overwriting argumentCollection, we deserialize it and then append its contents to the URL scope?
Application.cfc - Manual Deserialization With StructAppend()
<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, 5, 0 ) />
<!--- Define the log file path. --->
<cfset this.logFilePath = (
getDirectoryFromPath( getCurrentTemplatePath() ) &
"log.htm"
) />
<cffunction
name="onRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the request.">
<!---
Check to see if we have a serailized argument
collection that we can deserialize in order to
normalize the request.
--->
<cfif (
structKeyExists( url, "argumentCollection" ) &&
isSimpleValue( url.argumentCollection )
)>
<!---
Deserialize the collection and append it to the
incoming URL scope.
--->
<cfset structAppend(
url,
deserializeJSON( url.argumentCollection )
) />
<!---
Delete the argumentCollection entry so that the
ColdFusion framework doesn't try to use it.
--->
<cfset structDelete( url, "argumentCollection" ) />
</cfif>
<!--- Log the incoming URL scope. --->
<cfdump
var="#url#"
output="#this.logFilePath#"
label="URL - #getFileFromPath( cgi.script_name )#"
format="html"
/>
<!--- Return true so the page can process. --->
<cfreturn true />
</cffunction>
</cfcomponent>
If we use the structAppend() function to add the deserialized arguments to the URL scope, what we are doing essentially is converting the argumentCollection-based remote invocation into a format that we use more traditionally - one in which each invocation argument gets its own URL parameter. When we run our remote invocation with this Application.cfc version, our log file shows us a URL scope with the following format:
Since this format (one-to-one arguments) is one that ColdFusion has supported since it introduced remote-access methods, this works fine for the implicit web service. The benefit to this type of deserialization is that our URL scope is now packaged in such a way that if we wanted to step in and intercept the method invocation, we can do so without error. The only caveat is that the web service code cannot assume that it has a predefined argumentCollection - it can only assume that the URL scope contains all the necessary arguments.
Tip: The URL scope can, itself, be used as an argumentCollection.
I have never really been a big fan of remote-access methods; however, I am very happy that they have brought to light the ability to use argumentCollection with URL-based invocation. If for no other reason, a huge thanks to Ray and Stephen for getting me to think about alternative ways of passing data from the client to the server! This is some really exciting stuff!
Want to use code from this post? Check out the license.
Reader Comments
Woot! First comment.
@Ben, I'm having trouble as of late of trying to put a use to some of your posts. This is one of them.
What useful application might I want to know this information?
Since this is a web service, I'm guessing I'd consider doing this in order to, say, have a Google Map on my website and have it plot points? Give directions w/o leaving my domain? In other words, get information from another site and post it to it?
BTW - Does Katie know you think she looks super adorable? Maybe you should tell her. In real life. :-)
BTW, Got this error a couple of times: There was an error connecting to the API
@Ben,
So, as I understand it, the REST-style call (cfcname.cfc?method=methodname&arg1=value1&arg2=value2) can be passed argumentCollection and returnFormat (instead of arg1 and arg2 in this example). From my own experience with REST calls, I'm guessing that the default for returnFormat is WDDX.
Very interesting. Great to know.
It's strangely magical that CF web services can figure out on the fly whether the argumentCollection is JSON or WDDX. But I observe that the first non-blank character of JSON would always be left brace (or conceivably left bracket), and that the first non-blank character of WDDX would always be less-than. I'll bet that helps.
@Randall,
You ask the tough questions :) I just found out about this, so I don't have field-tested use cases for it yet. But, in does make passing complex data (ie. arrays, structs) to the server from the client (via AJAX) much than before. If you has previously asked me how to pass an array to the ColdFusion server, I'd say it takes some specialized parsing on the server. If you'd asked me how to pass a struct to the server, from the client... I am not sure I would have a good answer for you at all.
Being able to use the argumentCollection enables this complex data passing. Of course, if you don't need it, you don't need. I wouldn't "opt" for this approach if a standard name/value pair passing would work. This is simply a tool to deal with outlier cases perhaps?
@WebManWalking,
I used a returnFormat in my examples, but it's technically not needed. I believe it defaults to WDDX, but you can provide a default as part of the CFFunction tag (which can then be overridden in the REST call).
So would this be similar to passing an a params arg to a JS function and dynamically creating getters / setters?
Or would this even make sense to do in CF?
I guess server-side code it wouldn't make much sense.
I didn't read the article much... just saw it and thought of this.
I'll read it in a few!