I discovered a very bizarre side-effect (issue/bug?) this week in my journey as a ColdSpring developer. I would like to relate the story back in the hopes that it gets a bit more attention beyond the scope of the ColdSpring mailing list, where I first posed it earlier this week. I also want to thank Brian Kotek for his insightful responses to the issue, without which, I could possibly have gone mad.

Before I begin, I think it is interesting to note that the community is currently discussing the ramifications of getter/setter data-access in design, which directly relate to the issue I'm about to reveal.

To start, let's make certain we on the same table by clarifying a few disparate facts about ColdFusion technology. I'm going to gloss over some minutia to keep it concise; the purpose is not to insult any CF developers, it's to paint the picture of an obscure issue that involve a lot of factors. Feel free to correct me if I make an error in judgment below:

1. CF programmers use CFCs to build complex objects. A CFC-based object may include public and private properties, as well as any number of public or private methods.

2. CF programmers also use CFCs to build webservices. The guts of a CFC-based webservice are nearly indistinguishable from that of a standard object, except for the fact that the access level of the methods are no longer public or private, but are now "remote".

3. If a CF programmer intends for a CFC-based complex object to be webservice-ready, they will almost certainly use a tag called <CFPROPERTY>, which produces metadata for the object in across-the-wire transmission. When writing a webservice method that returns a value of this custom complex type, inialization of data is performed on the properties themselves, in the very same way you would access keys in a struct (ie. myObject.name = nameValue, myObject.synopsis = synopsisValue, etc.)

4. When a CFC is passed across the wire in a webservice call, the presence of <CFPROPERTY> tags provide consumers with the appropriate knowledge to deserialize the new type of data. If the deserialization process is performed by Axis (as is the case in ColdFusion), the aforementioned metadata is examined, and as it discovers complex types, exposes the properties of this complex type to the consuming app by dynamically generating getter/setter functions that encapsulate access to the said-data.

The end result: a get*() and set*() method is produced, as needed, for every matching property (keys in a struct) that is described to have read- and write-access. This is done at the time the object crosses the wire; it's perfectly acceptable to have a CFC contain only a list of <CFPROPERTY> tags, with no actual functions. Getters and Setters are still produced.

5. The ColdSpring framework, which uses dependency injection to "wire together" various CFCs in a CF application, implements a common methodolgy for defining a service layer. It is very common to see a ColdSpring application take a business object (for example, a magazine 'article'), and utilize the following CFCs:

articleService.cfc - The actual CFC responsible for providing public access to the data in a semi-abstracted format. There should typically be no references to data-store formats (ie. a particular database type)

articleGateway.cfc - A CFC responsible for producing multiple records of data to the service.

articleDAO.cfc - A CFC responsible for producing single-record CRUD access to the service.

article.cfc - A CFC bean, also referred to as a 'Value Object' or 'VO', which is a relatively flat description of the data, providing a list of getters/setters to each of the attributes of the article (ie. name: getName(), setName(); synopsis: getSynopsis(), setSynopsis(), etc.)

6. Since the articleDAO.cfc exclusively works with single-record data, it is also typically expected that the CRUD methods work in conjunction with the article.cfc VO, taking them in as arguments, populating them with data, and returning them to the service layer. The process of this data population (within the confines of the DAO) is commonly done one of two ways:

a) via the VO's init() method, which causes the VO to be initialized and populated with data at the same time, through a series of input arguments on the init() method that map to the actual data's attributes (ie. <cfargument name="synopsis" type="string"> etc.)

b) via the VO's explicit setter methods (ie. article.setSynopsis(data.synopsis), etc. )

In either case, encaspulation to the VO's private data is almost always assumed and enforced by the developer as a 'best-practice'.

7. As the above CFC dependency structure is so commonly used, there are a multitude of tools in existence that create these CFCs for you automatically. One of which is the highly-recommended Illudium PU-36 code generator. If you examine the guts of a VO that has been generated by Illudium, you'll note that it produces (among other things):

a) An appropriate get*() and set*() method for each attribute of the data.
b) A matching <CFPROPERTY> for each attribute of the data.

As a result, the generated VO is both ready for use as a standard bean within your ColdSpring application, and maintains the appropriate metadata if the type is to be used in across-the-wire communication via a webservice.

8. ColdSpring also provides a method for CF developers to take their pre-existing service layer (assume the one I used above, please!), and with little effort, expose that service layer via a webservice. It does this by the use of a Remote Proxy (which is extremely convenient, I might add). All the CF developer needs to do is add a simple bean declaration in their wire-up XML config file, and ColdSpring examines the service, auto-generating a matching webservice that, within it, contains a list of methods whose access type are "remote", and wrap the original service's function calls.

Phew! With me so far on all facts? Awesome. Now for the problem at hand...

--

Sally has been developing a ColdSpring-based web-application that allows users to read online magazine articles. The project has been in-development for a few months already, so the application is quite mature. Many different services are in-play in the ColdSpring wire-up XML file, and Sally has been making a real effort to stay consistent to the CS methodology, so her services depend upon gateways and DAOs, and the DAOs specifically work with Value Object-style beans.

One day during the dev-cycle, her manager informs her that a third-party is going to need access to the magazine article data. 'No sweat', she thinks. This is the perfect opportunity for the use of a Remote Proxy. The article service already contains a multitude of article-fetching data, she can simply create a Remote Proxy to her articleService.cfc in a few lines of XML in the wire-up file, and her mission is complete. She performs this task, and runs a few simple tests, consuming the WSDL and attempting to call her most basic method, getArticle(), which takes a single numeric argument, and returns an article VO.

What she gets, unfortunately, is a whole different can of worms.

First, she receives an obscure error from CF, which reads:

AxisFault faultCode: {http://schemas.xmlsoap.org/soap/
envelope/}Server.userException faultSubcode: faultString:
org.xml.sax.SAXException: Deserializing parameter 'getArticleReturn':
could not find deserializer for type {http://article.model}Article'

Most of that means very little to her, as she knows nothing about Axis and is not a Java developer. She is, however, able to examine the code generated by the Remote Proxy, and notices that in the webservice, all the method wrappers are of returntype='any'. She guesses that CF is unable to internally determine what a returntype of 'article' is, and hopes that it can be addressed by manually adding a <CFPROPERTY> to the top of the Remote Proxy, like so:

<cfproperty name="Article" type="model.article.article" />

She does this. Success! She is able to consume the WSDL, call getArticle(), and retrieve a variable that she can then use to access the various attributes of the data, such as the name of the article, a synopsis, and so on.

Sally takes her testing a step further, and performs the following call in her consumer template:

<cfoutput>#article.getSynopsis()#</cfoutput>

expecting to see the article's synopsis written to the screen. She, instead, gets nothing. She tries to access other getter methods on the article VO, like getName(), and getTitle(). Again, nothing.

So that she isn't going crazy, she <CFDUMP>s the return from getArticle(). Sure enough, there are all of her getters/setters as originally described in the article.cfc VO (which she happened to generate via Illudium).

She returns to articleService.cfc with the expectation that she is incorrectly passing an argument to getArticle(), or that somehow, the data is not being transmitted. Before the 'Article' VO is returned, she performs a <CFLOG> of the article's getSynopsis() result, right after it is created and populated by the DAO, but before it returns to the calling application.

She tests the webservice call again, and then opens up the generated log. Sure enough, the article's synopsis text is properly written to the log. Yet once again, in the consuming application, a call to getSynopsis() on the variable that is returned from getArticle() displays an empty string.

Sally re-examines the guts of the Remote Proxy that is generated by ColdSpring, looking to see what actually goes on in the getArticle() wrapper. She discovers the following:


<cffunction name="getArticle" access="remote" returntype="any" output="false">
<cfargument name="article_id" type="numeric" required="true" />

<cfset var rtn = callMethod('getArticle') />

<cfif rtn>
<cfreturn rtn />
</cfif>
</cffunction>


Sally notices something is missing. She undestands the need for a generic wrapper, callMethod(), which has the ability to call any type of function, and return any type of value, but she also knows about the restriction of passing a complex type back from a webservice (See above, #3).

Her assignment of data to the article VO, deep within the confines of articleDAO, is via an init() method, which is a struct key/value list passed as an argumentCollection. The assignment of data ends there. The article VO is that passed back through articleDAO, then through articleService, and then, through her Remote Proxy webservice, eventually ending up at a client consumer app.

At no time is a single <CFPROPERTY> key ever accessed, either to read, or to write. In other words, there is no denying she has a VO with the appropriate data (the <CFLOG> call confirmed it), but she never once preps the object for transmission across the wire, which in this case, requires calls to the properties (ie. article.synopsis = synopsisValue, article.head = headValue, etc.)

Furthmore, once the Remote Proxy returns the VO to the client app, it is flattened by Axis, it's original getter/setter methods are stripped away, and all-new getter/setter methods are implictly created, causing the illusion that Sally's VO has remained intact throughout the process, when in actuality, it is now an empty husk; a VO that never had its properties assigned before the webservice transmitted it, and a series of getters/setters that wrap those empty values, producing no results to the screen when called.

I hope you have enjoyed today's story!

Oh, what became of Sally, you ask? Well, she made two changes.

1. She modified the core ColdSpring files to produce a list of <CFPROPERTY> calls at the top of the auto-generated Remote Proxy webservice, whenever (during the createRemoteProxy() method) she discovers a returntype that is a custom CFC. This solves the deserialization issue wheh all the auto-generated wrapper methods have a returntype of 'any'.

2. She modified the callMethod() function so that if the 'rtn' variable is detected as a result of the original called method, she examines the metadata of the return. If she finds that it is a custom CFC type, she makes an assumption that it is a VO produced by Illudium and loops over the properties array of the rtn var, dynamically checking to see if there is a matching named get*() function. If there is, she assigns the return of the get*() method to the matching property before sending the 'rtn' variable back.

I, er, what I meant to say was.. SALLY...would be happy to produce this code upon request.

As Brian pointed out to me in the CS group, the change to callMethod() certainly has performance implications, which I why I'm hestitant to submit it directly as a patch to the CS team. A preferred technique would be during the call to AopProxyUtils::createRemoteMethod(), which would actually inject this code within the method wrapper, only applying the logic as-needed within specific functions. Unforutnately, the metadata of the return result (as it is communicated via Axis across the wire) is unavailable during the AopProxyUtils::createRemoteMethod() call, since no object actually exists and no data communication has actually executed; createRemoteMethod() is just a submethod of a larger function that does a file read of the named service and walks the actual cf tag structure to determine method names, input arguments, and return types.

Alas, this is where my research ends. Perhaps a more thoughtful CS framework teammember can provide an alternate solution that scales.