Thursday, February 4, 2010

Form Field Values, Multipart Forms and Servlets (... oh my)

I saw an intriguing question on stackoverflow yesterday, by Tony Petruzzi .  It was about parsing form field values, but with a bit of twist. We all know that when multiple form fields have the same name, the field values are returned as a comma delimited list in CF. While that is a handy feature at times, what do you do when the field values contain commas? Well after a few false starts, I did come up with one option, but wanted to describe it in a little more detail than stackoverflow comments allow.

Stealing from Tony's example, take the simple form below. Each of the two fields contains a different number of letters separated by a comma. But once the form is submitted, the value is just one big csv string. So you really have no way of determining which values were actually entered into each of the fields.



Tony's post described a slick method of extracting the field values as a string array, with the help of getPageContext() and getParameterMap(). The problem was it does not seem to work when the enctype is multipart/form-data.


<cfdump var="#getPageContext().getRequest().getParameterMap()#">

From what little I have read so far, my understanding of the issue is that the standard request object (ie HttpServletRequest) does not handle multipart requests.  A special multipart handler is needed instead. So with that in mind, my next question was what handler does CF use?

Well, while poking around in the CF libraries a while back, I noticed CF uses some of the O'Reilly servlet classes. In particular, the ones for MultiPartRequest handling. If you look at the API you will notice the classes return all sorts of good information like input field names, values, etcetera. So I figured that is probably what CF was using for the form and that this information had to be exposed ...somewhere.

On a whim I decided to get back to basics and dump the FORM scope class. That revealed a promising method named getPartsArray(). Looping through the array confirmed my suspicion that results were indeed one of the O'Reilly classes:
com.oreilly.servlet.multipart.ParamPart


<cfset formClass = createObject("java", form.getClass().name)>
   <cfdump var="#formClass#" label="FORM Scope Class" />
   
   <cfloop array="#form.getPartsArray()#" index="part">
      <cfdump var="#part#" label="Part Element" />
   </cfloop>


All that was left was to use a few methods from the API to the retrieve the form field information. First the isParamPart() method is used to identify the input fields only. Then getName() and getStringValue() are used to grab the field's name and value.  As you can see, there is not much to it.

<!--- if this is a multipart request ...--->
   <cfset variables.parts = form.getPartsArray()>
   <cfif structKeyExists(variables, "parts")>
      <cfoutput>   
      <cfloop array="#variables.parts#" index="p">
         <cfif p.isParam()>
            isParam() = #p.isParam()#
            getName() = #p.getName()#
            stringValue() = #p.getStringValue()#
         </cfif>
      </cfloop>
      </cfoutput>
   </cfif>


Of course this only applies to multipart requests. The part array will be null for other request types. So make sure the array exists before using it. Anyway, I thought this was an interesting approach to the old form field dilemma. But if anyone knows of any other techniques, I would love to hear them.

10 comments:

James Gibson February 4, 2010 at 4:34 PM  

Thanks for this post. This is incredible work and a real life saver for some features we are putting together for wheels.

cfSearching February 4, 2010 at 5:24 PM  

Glad I could make a small contribution to the wonderful wheels project :)

Unknown February 4, 2010 at 8:17 PM  

the man himself. you have no idea how thankful we are for your help. with this problem solved, james was able to start getting nested attributes working in cfwheels. if you want to see the finally product of your solution, take a look at the $getParameterMap() method in this commit: http://code.google.com/p/cfwheels/source/detail?r=4077

again, thank you very much for your help with this.

cfSearching February 4, 2010 at 8:57 PM  

Hey it is "Rip" ;) Pretty cool seeing it in action. Thanks! I also like the sound of the new object declarations feature. I will have to check that out whenever I get a break tomorrow (Now if only I could skip my meetings and do that instead ...)

Dan G. Switzer, II February 5, 2010 at 12:37 PM  

If you want to use purely supported native CF functionality, you can also use the GetHttpRequestData() function and parse the "content" key. It contains the raw data posted to the page.

You can then just parse through the string and build arrays out of fields that appear multiple times instead of building appended strings.

cfSearching February 5, 2010 at 1:02 PM  

@Dan,

Yes, I had tried that. But in my tests with CF9/Tomcat it did not seem to work for me with multipart requests. Does it actually work for you and if so, what configuration?

-Leigh

cfSearching February 5, 2010 at 1:21 PM  

One more thing. While I may be looking at it too simplistically, I was kind of assuming ...it should not work. I imagined the information from GetHttpRequestData() would mirror ServletRequest. So if ServletRequest does not normally handle multipart information, then GetHttpRequestData() would not do so either.

But I could be totally off in left field there. Anyway, that was the thought process ;)

-Leigh

John Allen February 5, 2010 at 2:24 PM  

This is very very cool information.

cfSearching February 6, 2010 at 3:05 PM  

I had to install CF9+built in webserver for another issue. So I was able to confirm getHttpRequestData().content does not work for multipart.

Now I suspect I am still a bit off kilter about the "why" of it, due to my ignorance of exactly what steps occur in CF when the request occurs. As well as which classes are involved. But suffice it to say, from getHttpRequestData()'s perspective, "content" seems to be empty when the request is multipart. Possibly because the raw stream was processed already.

Anyway, maybe someone more knowledgeable will chime in and enlighten me about the "why" part (no pun intended).

-Leigh

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep