I had some information ready to be posted here when I submitted my entry into Ray's Best of CF9 contest, but he covers it pretty well in his review post; and he was kind enough to host the video on his S3 account. I've also posted the video on my Vimeo account, so here it is again for reference:
You can download the source code here. Unfortunately, I haven't found any more time to continue working on this; and I have to admit that as cool as the CFaaS feature is, I'm not terribly motivated to go further with this mail client because I know it will eventually get canned. Why write a mail client when you use Outlook every day?
So it was a fun way to play with Flex 4, the latest Swiz build at the time, AIR, and CFaaS; but I don't think I'll bother continuing to mess with it.
Sometimes I find myself sending a query object from ColdFusion to Flex, for example, to bind to a DataGrid, and then I want to edit that query object and return it to ColdFusion to be saved in the database. Unfortunately, Queries are only a data type in ColdFusion. When sent to Flex, they get mapped to an ArrayCollection, and when you send that ArrayCollection back to ColdFusion, it isn't automatically converted back to a Query object (though lord knows that would be nice!).
It's certainly possible to just treat that ArrayCollection as an array of structures — because that's what it is — and loop over it to extract the data you want. Sure. Be my guest. But ColdFusion has so many great functions designed to make working with Queries easier. Wouldn't it be great if you could just convert that ArrayCollection back to a Query?
That's why I created this UDF, and I intend to submit it to CFLib once I know it's solid. I wanted to put this out to the community though to make sure it covers all of the necessary data types and doesn't have any errors. Can you see anything I've missed?
Below is the code for the function as well as a unit test. To see how it handles bad/unexpected data, you can do something wacky like change one of the values inside the structs to be a struct instead of the simple value it is now.
<cffunction name="arrayCollectionToQuery">
<cfargument name="arrayColl" type="Array" required="true" />
<cfset var qResult = '' />
<cfset var columnList = structKeyList(arrayColl[1]) />
<cfset var typeList = ''/>
<cfset var numericType = ''/>
<cfset var k = '' />
<cfset var i = 0 />
<cfloop collection="#arrayColl[1]#" item="k">
<cfif isNumeric(arrayColl[1][k])>
<!--- decimal or integer? --->
<cfset numericType = "integer">
<cfloop from="1" to="#arrayLen(arrayColl)#" index="i">
<cfif arrayColl[i][k] - fix(arrayColl[i][k]) gt 0>
<cfset numericType = "decimal" />
<cfbreak />
</cfif>
</cfloop>
<cfset typeList = listAppend(typeList, numericType) />
<cfelseif isSimpleValue(arrayColl[1][k])>
<cfset typeList = listAppend(typeList, 'varchar') />
<cfelseif isBoolean(arrayColl[1][k])>
<cfset typeList = listAppend(typeList, 'bit') />
<cfelseif isDate(arrayColl[1][k])>
<cfset typeList = listAppend(typeList, 'date') />
<cfelse>
<cfthrow message="Invalid ArrayCollection"
detail="All keys in your array collection must be of one of the following types: Numeric (Int or Float), String, Boolean, Date. The following key contains data that is not one of these types: `#k#`" />
</cfif>
</cfloop>
<cfset qResult = queryNew(columnList, typeList) />
<cfloop from="1" to="#arrayLen(arrayColl)#" index="i">
<cfset queryAddRow(qResult) />
<cfloop collection="#arrayColl[i]#" item="k">
<cfif not isNumeric(arrayColl[i][k]) and not isSimpleValue(arrayColl[i][k]) and not isBoolean(arrayColl[i][k]) and not isDate(arrayColl[i][k])>
<cfthrow message="Invalid ArrayCollection"
detail="All keys in your array collection must be of one of the following types: Numeric (Int or Float), String, Boolean, Date. The following key contains data that is not one of these types: `#k#`" />
</cfif>
<cfset querySetCell(qResult,k,arrayColl[i][k]) />
</cfloop>
</cfloop>
<cfreturn qResult />
</cffunction>
<cfset testData = [
{Num=2, String='fubar!', Bool=true, Date=CreateDate(2009,05,2)},
{Num=4.8, String='bufar!', Bool=false, Date=CreateDate(2009,06,1)},
{Num=6, String='futbol!', Bool=true, Date=CreateDate(2009,07,12)},
{Num=8, String='string!', Bool=false, Date=CreateDate(2009,08,9)},
{Num=10, String='data type!', Bool=true, Date=CreateDate(2009,09,18)},
{Num=12, String='yes', Bool=false, Date=CreateDate(2009,10,6)}
] />
<cfdump var="#testData#" label="data in">
<cfdump var="#arrayCollectionToQuery(testdata)#" label="data out">
Personally, I'm pretty excited about this. Peter Elst posted a video from AdobeTV of Ryan Stewart demoing a feature that might be in the next version of ColdFusion. As a matter of fact, here's the comment I left on Peter's post:
Holy @%^&*@#%%^&%@##$%.
So what's this cool new feature? It's actually something being developed by the ColdFusion team, not the Flex team. Essentially, what it boils down to is that by adding a line of configuration code pointing to your ColdFusion server, and setting up a user account on that CF server that has this new remote access enabled, you can access some core ColdFusion functionality from your Flex application without writing a single line of ColdFusion code.
In his example, Ryan uploaded a PDF file, and used CFPDF remotely from his Flex application to access the meta-information about that PDF from Flex — all without writing any CFML. (I'm guessing that the upload itself required a few lines of CFML, but that wasn't what was being demoed.)
It sounded like Ryan said that access can be restricted per-user to specific tags, so it should be highly configurable.
I paused the video at about 2:10, and it looks like these tags are already supported: CFChart, CFDocument, CFImage, CFLDAP, CFMail, CFPDF, and CFPOP. This list has the potential to grow between now and release, too. I would bet they only have code-hinting enabled for features that are already functional, and I'm sure they're hard at work finding and implementing other great tags. I wouldn't be surprised to see CFExchange, CFDBInfo, and a few other tags added to that list. (How cool would it be to have a Flex app to administer MySQL in the same manner as phpMyAdmin?)
Ryan says that this feature "may be in ColdFusion 9, code named 'Centaur'." Let's be real about this: When has Adobe ever demoed a feature or product that wasn't all but guaranteed to be released? They are too careful to do something like that. So this may change form a little bit, but I fully expect it to be included, in some way, shape, or form.
When it comes down to it, this isn't incredibly ground-breaking, because you can already accomplish all of this by writing some server side code. But what's happening here is that the ColdFusion engineering team is doing what they do best: making tedious or complex things easy.
Most of all, it made me realize that we need to start thinking more about the status quo in our applications. How many flex applications do you have that require a username and password that get handed off to be handled by your hand-written server-side LDAP authentication code? You can do that automatically as the application loads, and the user doesn't even have to think about signing in, but still gets their personalized interface. You can do that today, but with these new "Proxy Tags" it would require less code.
I think ColdFusion 9 is poised to be a game changing release. Are you ready?
As part of a project where I work, I had the opportunity to refactor and open-source a Flex application that is used as a multi-file upload tool. This beats the pants off of N <input type="file" /> fields. It works somewhat like the flash upload you might be used to on Flickr. You can use it with ColdFusion, PHP, Ruby, or any other server-side language that can accept file uploads.
… and it can be yours for the low, low price of free! You can include it in any application to make uploading multiple files easier and more usable.
Using it requires 2 separate templates: one to display the Flex widget, and another to accept the incoming files, just as if they were posted from a standard web form (it is posted to once per file). Since most people are somewhat familiar with traditional file upload, let's start there.
Accepting uploaded files
Here's some sample code in CFML that handles the posts from the Flex application. It is exactly the same as what you would use with a traditional form-based file upload, with the exception that flash uploads everything with the mime type "application/octet-stream," so we can't use that to determine the uploaded file type and instead have to rely on the file extension.
<!---
Flex Multi-File Upload Server Side File Handler
===============================================
This file is where the upload action from the Flex Multi-File Upload
UI points. This is the server side half of the upload process.
--->
<cftry>
<!---
Flash uploads all files with a binary mime type
("application/ocet-stream"), so we can't set cffile to accept
specfic mime types. The workaround is to check file type after
it arrives on the server and if it is not desireable, delete it.
--->
<cffile action="upload"
filefield="filedata"
destination="#application.xmlUploadDest#"
nameconflict="makeunique"
accept="application/octet-stream"
/>
<!--- If the file extension isn't allowed, delete it --->
<cfif not listFindNoCase(acceptedFileExtensions,File.ServerFileExt)>
<cffile
action="delete"
file="#application.xmlUploadDest#/#File.ServerFile#"
/>
</cfif>
<cfcatch type="any">
<!--- bubble up errors to onError in Application.cfc --->
<cfthrow object="#cfcatch#">
</cfcatch>
</cftry>
It's pretty straight-forward. A file is uploaded by the Flash player, very similar to posting a form with an <input type="file" name="filedata" />. The server will see the request almost exactly as if a form was submitted with a file upload — the form field name for the file is "filedata", that is hard coded. The script above is hit once per file being uploaded. The script saves the file to its final destination, and if there are any errors they bubble up to the onError method in Application.cfc.
The error bubbling is important, because the flash player won't display issues to the user, and it won't let you — the developer — know if there are problems. By throwing an error, you can catch it and email or otherwise log the issue so that you have some information to help you resolve whatever the problem may be.
See? I told you it was simple.
Embedding the Flex Widget
Next up, we need to put that Flex widget in your web application, and configure it to look and run as you need it to. There are a few parameters you can specify in order to change its appearance and behavior:
Parameter
Required
Default
Description
uploadDest
yes
n/a
The URL for the script that accepts each uploaded file (the one we discussed above).
fileTypeFilter
yes
n/a
A semicolon-delimited list of file types to show in the browse dialog. (Eg. "*.xml; *.pdf; *.doc")
fileTypeFilterName
yes
n/a
A string that describes what filters are being applied. Often, this also includes the filters. (Eg. "Documents (*.xml, *.pdf, *.doc)")
sessionParams
probably ;)
n/a
This semi-optional parameter is only required for Alternative Browser support. If you are working in a closed environment and IE will be mandated, you can ignore it. It is used to work around a Flash file upload bug. I'll discuss the details of what you should include here further below.
title
no
"File Upload"
Displayed at the top of the panel. In the screenshot above, this was set to "SABRE File Upload".
uploadCompleteMessage
no
"Upload Complete"
A string to display on the progress bar once upload is complete.
maxSize
no
2048
Max file size that Flash will let the user upload. Specify in kilobytes. (Default of 2048 = 2mb)
callback
no
n/a
The name of a JavaScript function defined (or included) on the page that includes the Flex application. This function will be run when the upload is complete, and gives you the ability to have your application be aware of when files have been uploaded.
How do you set these values when including a Flash/Flex widget on your page? Via FlashVars. They look and act a lot like a standard URL, so even if you've never heard of them before, you're already half way there. FlashVars look like this: foo=bar&now=then. See? Familiar!
You'll want to use JavaScript to embed the flash player on the page, like so:
<cfscript>
variables.flashvars = "uploadDest=#urlEncodedFormat(expandPath('uploadedFiles/'))#";
variables.flashvars &= "&title=SABRE%20File%20Upload";
variables.flashvars &= "&fileTypeFilter=#urlEncodedFormat('*.xml')#";
variables.flashvars &= "&fileTypeFilterName=#urlEncodedFormat('XML Documents (*.xml)')#";
//... and so on, for other flashvars
</cfscript>
<script src="AC_OETags.js" language="javascript"></script>
<script type="text/javascript">
AC_FL_RunContent(
"src", "FlexFileUpload", //path to SWF without ".swf" -- in this case, it's in the same directory
"width", "660",
"height", "350",
"align", "middle",
"id", "FlexFileUpload",
"quality", "high",
"bgcolor", "##ffffff", //enter the bg color of your page so it will match while loading
"name", "FlexFileUpload",
"flashvars",'#variables.flashVars#', //this is where flashvar values are sent to the flash player
"allowScriptAccess","sameDomain",
"type", "application/x-shockwave-flash",
"pluginspage", "http://www.adobe.com/go/getflashplayer"
);
</script>
<noscript>
<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
id="FlexFileUpload" width="100%" height="100%"
codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
<param name="movie" value="FlexFileUpload.swf" />
<param name="FlashVars" value="#variables.flashVars#" />
<param name="quality" value="high" />
<param name="bgcolor" value="##869ca7" />
<param name="allowScriptAccess" value="sameDomain" />
<embed src="FlexFileUpload.swf"
FlashVars="#variables.flashVars#"
quality="high" bgcolor="##ffffff"
width="660" height="350" name="FlexFileUpload" align="middle"
play="true"
loop="false"
quality="high"
allowScriptAccess="sameDomain"
type="application/x-shockwave-flash"
pluginspage="http://www.adobe.com/go/getflashplayer">
</embed>
</object>
</noscript>
There may be better ways of embedding the Flash/Flex widget on the page, but I'm still new to this. Feel free to enlighten me with cleaner, more elegant code.
Alternative Browser Support
As I mentioned in the table above, there is a bug in recent versions of the Flash player that prevents file uploads from working out of the box in Alternative (Non-IE) browsers. I could go into more detail of why, but unless you want to hear about Network Stacks (in which case, email me), I'll spare you.
The short version of the story is that the Flash player doesn't maintain your existing website session, so if an authenticated session is required to access the template that accepts the uploaded file(s), the web server refuses access, and thus uploads won't work. To get around this, you have to include some extra URL parameters when uploading the file so that the client's local machine will include certain cookies in the request, which will tell the web server to tie this request to your existing session, and allow the files to be uploaded.
In order for the flash player to include those URL parameters, you have to tell it what they are. (They differ per server technology. In ColdFusion with J2EE sessions enabled, it's jsessionid. In ColdFusion without J2EE sessions enabled, you need CFID and CFToken.) So for example, in SABRE — the application that I've refactored this widget out of — here are my flashvars. Pay special attention to variables.sessionParams. That is what we're using to work around the Flash upload bug.
Now, I'm no expert on licensing, and generally I release my open source stuff under the Apache License. However, this application is heavily based on some code written by "Ryan Favro and New Media Team Inc." (no url provided). Their code was released under GPLv2, which I believe requires that modifications such as this one also be released under the same license. As such, this application and all supporting files are released under the GNU General Purpose License (GPL) version 2. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
Download
The zip contains a working example that you can put on a ColdFusion server to see in action. You can also try a demo. Of course, all uploaded files will be automatically and immediately deleted, because you can't trust the internet — so don't get any ideas.
The example also shows how to use the JavaScript callback on upload completion, if you want to use that to automate further action in your application.