On September 17, 2011 wrote: Handling Multiple File Uploads With Uploadify
More JavaScript fun this week, during which I have been playing around with Uploadify, one of a slew of ready-made JavaScript + Flash solutions for handling multiple file uploads in the browser. I have chosen to look at Uploadify over the other alternatives due to the fact that I find it personally to be the most simple to use. Also, it is provided as an extension to jQuery, which is probably my favourite JavaScript library. Last, but not least, it works … which is a quality never to be underestimated in software!
As usual, I am going to be demonstrating its use in conjunction with a Spring-Velocity web app. Rather than go through the whole process of setting up such an app again, I would refer you to my post on ActiveMQ, JMS & Ajax. The structure of the project I talk about here is identical to that, but with fewer dependencies, and, of course, you can download the code if you want to take a closer look. So let’s begin …
Let’s begin by imagining what our use case here might look like. For the sake of argument, I am going to say that what we want is this:
- The user needs to be able to upload one or more files.
- The names of these files must match a naming convention from which the application infers some kind of semantic value (which might be used to process them in different ways, for example).
- When the user uploads valid file(s), they should see a list of the files uploaded with the semantic information we have parsed from them via their name(s).
- When the user uploads invalid file(s), they should see information about this error.
In my domain model, therefore, I have 2 classes:
public enum UploadFileType {
BAR(Pattern.compile("(?i)^[a-z0-9]+-bar\\.[a-z]+$")),
BAZ(Pattern.compile("(?i)^[a-z0-9]+-baz\\.[a-z]+$")),
FOO(Pattern.compile("(?i)^[a-z0-9]+-foo\\.[a-z]+$"));
public static UploadFileType forName(String name) {
for (UploadFileType type : values()) {
if (type.p.matcher(name).matches()) return type;
}
throw new IllegalArgumentException(name + " does not match any of the patterns in the naming convention");
}
private Pattern p;
private UploadFileType(Pattern p) {
this.p = p;
}
}
Because the logic required to parse my naming convention is simple, I do not really need any kind of complex factory … all I need is this simple enum. You give it the file name in the forName(String) method and it returns the type. If no type is found, we throw an IllegalArgumentException which we will use later to manage error flow. After this, I have a simple Java bean to represent the model for an UploadFile:
public class UploadFile {
private final File file;
private final String name;
private final UploadFileType type;
public UploadFile(File file, String name, UploadFileType type) {
this.file = file;
this.name = name;
this.type = type;
}
@Override public boolean equals(Object obj) {
return reflectionEquals(this, obj);
}
public File getFile() {
return file;
}
public String getName() {
return name;
}
public UploadFileType getType() {
return type;
}
@Override public int hashCode() {
return reflectionHashCode(this);
}
@Override public String toString() {
return reflectionToString(this);
}
}
Now, let’s assume I have a web page with a completely standard HTML form with a file input:
<form id="upload_file" action="/upload" method="post" enctype="multipart/form-data">
<fieldset>
<legend>Choose the files to upload</legend>
<label for="file">File</label> <input id="file" name="file" type="file" /> <input type="submit" value="Upload" />
</fieldset>
</form>
And, on the server-side, using Spring’s multipart form support, I have a FileController that looks like this:
@Controller public class FileController {
@RequestMapping(value = "/", method = RequestMethod.GET) public String getFileUploadViewName() {
return "/fileupload";
}
@RequestMapping(value = "/upload", method = RequestMethod.POST) public UploadFile upload(@RequestParam("file") MultipartFile mf) throws IOException {
String name = getName(mf.getOriginalFilename());
File f = File.createTempFile(name, "." + getExtension(name));
mf.transferTo(f);
return new UploadFile(f, name, UploadFileType.forName(name));
}
@ExceptionHandler(IllegalArgumentException.class) @ResponseBody public String handleIllegalArgumentException(IllegalArgumentException e) {
return "{\"error\":{\"message\":\"" + e.getMessage() + "\"}}";
}
}
For the sake of clarity, here is what each of the three controller methods do:
getFileUploadViewName()simply returns the Spring view name required to return the HTML file upload form. I have hard-coded this name for now but, in real life, you should probably inject this as configuration.upload(MultipartFile)is the meat of the controller class: Spring will bind the multipart file from the file input of the submitted form which hasname="file". The only processing we are doing with this file for now is creating a temp file from it and determining its “type” by using theUploadFileType.forName(String)method. This will either return the type (in which case we have successfully anUploadFilewhich we return as the model object) or it will throw theIllegalArgumentExceptionin which case, our Spring error handler method is called …- The
handleIllegalArgumentExceptionmethod is a Spring error handler method (indicated by the@ExceptionHandlerannotation. In such an event, all we do is return a hand-crafted JSON string as the response body with the exception message in it … which is a bit lazy, I admit — but it will do for now.
With this in place, we have an end-to-end convention HTML single-file upload mechanism in place. So now it is time to decorate it with a little JavaScript magic so that the whole thing becomes a bit more usable and so that it becomes possible for the user to upload multiple files.
First, to provide the user with the necessary feedback in the page from the file uploads, I am going to add a few placeholder sections to my HTML page within which I will display data from the parsed JSON responses provided by the controller …
<section id="file_queue">
<hgroup>
<h1>File Upload Queue</h1>
<div id="queue">
</div>
</hgroup>
</section>
<section id="files">
<hgroup>
<h1>Uploaded Files</h1>
<ol>
</ol>
</hgroup>
</section>
<section id="errors">
<hgroup>
<h1>Errors</h1>
<ol>
</ol>
</hgroup>
</section>
We add, first, a section that will be used by Uploadify to display the queue of files as they are being uploaded with the ID “queue”. This is a built-in feature of Uploadify, so we don’t need to do anything special to get this queue displayed, other than let Uploadify know the ID of our “queue div” by setting the queueID property. After that, I have placed the 2 sections that we will be writing the info from the parsed JSON responses from the controller into — 1 for successful file uploads and 1 for errors.
Now I am just about ready to sprinkle a little JavaScript on the page … But, stop! Before I do this there is ONE VERY IMPORTANT GOTCHA to deal with!
Now, due to the nature of the genius minds that work at Adobe, it is apparently impossible to set HTTP headers in requests made by Flash (I know, I know — what kind of nonsense is that?!) I am not a Flash expert, so I don’t know the ins-and-outs of this. However, since Uploadify (and pretty much any JavaScript-based multifile uploader) uses swfobject.js and Flash, this means that an Accept header cannot be set to request JSON responses. By default, as far as I can work out, Flash will always send an Accept header requesting text/*. If you are using Spring’s ContentNegotiatingViewResolver (which I am in this example), you will need to work around this problem. There are 3 options: the first is to use a .json file extension on the request URI to indicate the required media type for the response this way. The second option is to use a request parameter to achieve the same thing. The final way is to filter the request and override the Accept header for Uploadify requests.
In real life, I would probably just send Uploadify requests to /upload.json and then the ContentNegotiatingViewResolver would know to send a JSON response. However, for whatever reason, you might not want or be able to change the URI in this way, so I will demonstrate the Old Skool servlet filter approach (you might think you could also use a Spring HandlerInterceptor but that leads to call-order problems with the ContentNegotiatingViewResolver — using a classic filter will ensure that the request is intercepted and wrapped before it even reaches Spring:
public class UploadifyFilter implements Filter {
@SuppressWarnings({ "deprecation", "rawtypes" }) private static class UploadifyRequest implements HttpServletRequest {
private final HttpServletRequest del;
UploadifyRequest(HttpServletRequest delegate) {
del = delegate;
}
@Override public String getHeader(String name) {
return "Accept".equals(name) ? "application/json" : del.getHeader(name);
}
/* ... all other delegate methods not shown for brevity ... */
}
@Override public void init(FilterConfig filterConfig) throws ServletException {}
@Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
if (req instanceof HttpServletRequest) chain.doFilter(new UploadifyRequest((HttpServletRequest) req), resp);
else chain.doFilter(req, resp);
}
@Override public void destroy() {}
}
I then simply wire this up in my web.xml to filter requests sent to the /upload URI:
<filter>
<filter-name>uploadify</filter-name>
<filter-class>com.christophertownson.foo.UploadifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>uploadify</filter-name>
<url-pattern>/upload</url-pattern>
</filter-mapping>
And now I am ready to receive Accept: text/* requests from the “Shockwave Flash” user-agent and ensure that my app understands what content-type it should return. So let’s put in some JavaScript to make it all happen …
function init() {
$('#file').uploadify({
'script' : '/upload', // URI to send the files to (could specify '/upload.json' here rather than using filter)
'method' : 'post', // request method to use
'uploader' : '/resources/uploadify.swf', // path to key uploadify files
'expressInstall' : '/resources/expressInstall.swf', // path to key uploadify files
'cancelImg' : '/resources/cancel.png', // path to key uploadify files
'auto' : true, // automatically start uploading files (don't wait for user to push another button)
'multi' : true, // allow upload of multiple files
'queueSizeLimit' : 10, // the number of files that can be placed into the queue at any 1 time
'queueID' : 'queue', // div ID to display the file upload queue in
'fileDataName' : 'file', // the request param name for the multipart file
'fileExt' : '*.txt;*.jpg;*.gif;*.png', // filter to use in the select files dialog
'fileDesc' : 'Upload Files (.txt, .jpg, .gif, .png)', // we MUST provide a description when we specify fileExt otherwise filter will NOT be applied
// process JSON response to single file upload
'onComplete' : function(event, id, file, resp, data) {
var obj = jQuery.parseJSON(resp);
if (obj.error) {
$('#errors ol').append('<li>' + obj.error.message + '</li>');
} else if (obj.uploadFile) {
$('#files ol').append('<li>' + obj.uploadFile.name + ' (Type: ' + obj.uploadFile.type +')</li>');
}
},
// process HTTP 5xx errors etc
'onError' : function (event, id, file, err) {
$('#errors ol').append('<li>' + err.type + ' Error: ' + err.info + '</li>')
}
});
$('#upload_file').submit(function() {
$('#file').uploadifyUpload();
return false;
});
}
$(document).ready(init);
It should be fairly evident what this simple JavaScript achieves but I will run through it briefly:
- It begins by configuring some basic Uploadify properties including, perhaps most important for my purposes here
'multi' : trueas this is what permits the upload of multiple files. - After that, we provide an event callback handler function for the
onCompleteevent — this event signals completion of the upload of an individual file within the queue and provides access to the response from the server. In our case, this consists of a JSON string which we deserialize and append to the list of upload files or errors, as necessary. I also hook into theonErrorevent to provide similar error handling for Uploadify errors (rather than the logical exceptions thrown by our controller that come back as JSON) … these are commonly things like HTTP 5xx errors and so forth. - Finally, I attach a listener to the
onSubmitevent of the upload file form to prevent normal submission and, instead, to get Uploadify to handle the input.
There are many configuration options and possibilities for event handling within the API. I would recommend reading the wonderfully concise Uploadify documentation and it really is an exceptionally easy piece of kit to use, especially compared with the pain I recall trying to get some of the early versions of the YUI uploader to work nicely a few years back … although the YUI3 Uploader looks a great improvement on previous versions and is widely used and would also be well-worth considering.