More JavaScript fun this week, dur­ing which I have been play­ing around with Upload­ify, one of a slew of ready-made JavaScript + Flash solu­tions for han­dling mul­ti­ple file uploads in the browser. I have cho­sen to look at Upload­ify over the other alter­na­tives due to the fact that I find it per­son­ally to be the most sim­ple to use. Also, it is pro­vided as an exten­sion to jQuery, which is prob­a­bly my favourite JavaScript library. Last, but not least, it works … which is a qual­ity never to be under­es­ti­mated in software!

As usual, I am going to be demon­strat­ing its use in con­junc­tion with a Spring-Velocity web app. Rather than go through the whole process of set­ting up such an app again, I would refer you to my post on ActiveMQ, JMS & Ajax. The struc­ture of the project I talk about here is iden­ti­cal to that, but with fewer depen­den­cies, and, of course, you can down­load the code if you want to take a closer look. So let’s begin …

Let’s begin by imag­in­ing what our use case here might look like. For the sake of argu­ment, 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 nam­ing con­ven­tion from which the appli­ca­tion infers some kind of seman­tic value (which might be used to process them in dif­fer­ent ways, for example).
  • When the user uploads valid file(s), they should see a list of the files uploaded with the seman­tic infor­ma­tion we have parsed from them via their name(s).
  • When the user uploads invalid file(s), they should see infor­ma­tion about this error.

In my domain model, there­fore, 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 nam­ing con­ven­tion is sim­ple, I do not really need any kind of com­plex fac­tory … all I need is this sim­ple 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 man­age error flow. After this, I have a sim­ple Java bean to rep­re­sent 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 com­pletely stan­dard 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 mul­ti­part form sup­port, 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 clar­ity, here is what each of the three con­troller meth­ods do:

  1. getFileUploadViewName() sim­ply 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 prob­a­bly inject this as configuration.
  2. upload(MultipartFile) is the meat of the con­troller class: Spring will bind the mul­ti­part file from the file input of the sub­mit­ted form which has name="file". The only pro­cess­ing we are doing with this file for now is cre­at­ing a temp file from it and deter­min­ing its “type” by using the UploadFileType.forName(String) method. This will either return the type (in which case we have suc­cess­fully an UploadFile which we return as the model object) or it will throw the IllegalArgumentException in which case, our Spring error han­dler method is called …
  3. The handleIllegalArgumentException method is a Spring error han­dler method (indi­cated by the @ExceptionHandler anno­ta­tion. In such an event, all we do is return a hand-crafted JSON string as the response body with the excep­tion mes­sage 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 con­ven­tion HTML single-file upload mech­a­nism in place. So now it is time to dec­o­rate it with a lit­tle JavaScript magic so that the whole thing becomes a bit more usable and so that it becomes pos­si­ble for the user to upload mul­ti­ple files.

First, to pro­vide the user with the nec­es­sary feed­back in the page from the file uploads, I am going to add a few place­holder sec­tions to my HTML page within which I will dis­play data from the parsed JSON responses pro­vided 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 sec­tion that will be used by Upload­ify to dis­play the queue of files as they are being uploaded with the ID “queue”. This is a built-in fea­ture of Upload­ify, so we don’t need to do any­thing spe­cial to get this queue dis­played, other than let Upload­ify know the ID of our “queue div” by set­ting the queueID prop­erty. After that, I have placed the 2 sec­tions that we will be writ­ing the info from the parsed JSON responses from the con­troller into — 1 for suc­cess­ful file uploads and 1 for errors.

Now I am just about ready to sprin­kle a lit­tle 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 appar­ently impos­si­ble to set HTTP head­ers in requests made by Flash (I know, I know — what kind of non­sense is that?!) I am not a Flash expert, so I don’t know the ins-and-outs of this. How­ever, since Upload­ify (and pretty much any JavaScript-based mul­ti­file uploader) uses swfobject.js and Flash, this means that an Accept header can­not be set to request JSON responses. By default, as far as I can work out, Flash will always send an Accept header request­ing text/*. If you are using Spring’s ContentNegotiatingViewResolver (which I am in this exam­ple), you will need to work around this prob­lem. There are 3 options: the first is to use a .json file exten­sion on the request URI to indi­cate the required media type for the response this way. The sec­ond option is to use a request para­me­ter to achieve the same thing. The final way is to fil­ter the request and over­ride the Accept header for Upload­ify requests.

In real life, I would prob­a­bly just send Upload­ify requests to /upload.json and then the ContentNegotiatingViewResolver would know to send a JSON response. How­ever, for what­ever rea­son, you might not want or be able to change the URI in this way, so I will demon­strate the Old Skool servlet fil­ter approach (you might think you could also use a Spring HandlerInterceptor but that leads to call-order prob­lems with the ContentNegotiatingViewResolver — using a clas­sic fil­ter will ensure that the request is inter­cepted 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 sim­ply wire this up in my web.xml to fil­ter 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 “Shock­wave Flash” user-agent and ensure that my app under­stands 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 evi­dent what this sim­ple JavaScript achieves but I will run through it briefly:

  1. It begins by con­fig­ur­ing some basic Upload­ify prop­er­ties includ­ing, per­haps most impor­tant for my pur­poses here 'multi' : true as this is what per­mits the upload of mul­ti­ple files.
  2. After that, we pro­vide an event call­back han­dler func­tion for the onComplete event — this event sig­nals com­ple­tion of the upload of an indi­vid­ual file within the queue and pro­vides access to the response from the server. In our case, this con­sists of a JSON string which we dese­ri­al­ize and append to the list of upload files or errors, as nec­es­sary. I also hook into the onError event to pro­vide sim­i­lar error han­dling for Upload­ify errors (rather than the log­i­cal excep­tions thrown by our con­troller that come back as JSON) … these are com­monly things like HTTP 5xx errors and so forth.
  3. Finally, I attach a lis­tener to the onSubmit event of the upload file form to pre­vent nor­mal sub­mis­sion and, instead, to get Upload­ify to han­dle the input.

There are many con­fig­u­ra­tion options and pos­si­bil­i­ties for event han­dling within the API. I would rec­om­mend read­ing the won­der­fully con­cise Upload­ify doc­u­men­ta­tion and it really is an excep­tion­ally easy piece of kit to use, espe­cially com­pared with the pain I recall try­ing to get some of the early ver­sions of the YUI uploader to work nicely a few years back … although the YUI3 Uploader looks a great improve­ment on pre­vi­ous ver­sions and is widely used and would also be well-worth considering.