Friday, February 4, 2011

Handling file uploads with Java EE 6 / JAX-RS / Glassfish / Uploadify

UPDATE: This article is very obsolete. RichFaces 4, among other framworks, now handles multi-file upload out of the box. Use that instead.

Joeri Sykora wrote about using Jersey extensions to JAX-RS to handle file uploads in Java EE 6. His example is extremely handy, but needs some updates to handle Jersey 1.5.

Update: I've now put together a complete and self contained Java EE upload application using Uploadify, which you can grab from github.com/ringerc/postupload. See http://blog.ringerc.id.au/2011/03/file-upload-webapp.html

Jersey 1.5 and jersey-multipart's @FormDataParam

Jersey 1.5 advises that the jersey-multipart contrib module be used to handle uploads, using the multipart/form-data content type and @FormDataParam annotation.

Joeri's JAX-RS code may be adapted to read:

@Path("/file")
public class FileHandler {

  @POST
  @Path("/upload")
  @Consumes("multipart/form-data")
  @Produces("text/plain")
  public String uploadFile(
          @FormDataParam("file") InputStream file,
          @FormDataParam("file") FormDataContentDisposition fileInfo) {

    // your code here to copy file to destFile
    System.err.println("Received file: " +  fileInfo.getFileName() + " as " + file);

    return "1";
  }

}

... so that the file receipt is handled more efficiently.

Determining the correct path to the upload handler

Determining the URL to the upload handler isn't trivial. JAX-RS doesn't understand ../ paths, so faces/../rest/file/upload isn't the same as rest/file/upload to it. We can't easily create an absolute path, either, because the application is deployed in a configurable context root inside the Java EE server.

The easiest way to do it is build an absolute URL where you substitute the context root in. Assuming your JAX-RS servlet is configured to handle rest/* in web.xml and your upload handler is at file/upload in JAX-RS, you might use:

$(function() {
  $('#file_upload').uploadify({
    'script' : '#{facesContext.externalContext.request.contextPath}/rest/file/upload',
    // blah blah blah rest of uploadify config 
  });
});

Handling form data on a page that accepts Uploadify file uploads

An additional wrinkle is that Joeri's method isn't suitable for use where you want to have a <h:form> submit form data after all files are uploaded. If you attach the uploadify uploadifyUpload() call to a JSF commandLink or commandButton you'll find that the form submits before all the files upload. This is because uploadifyUpload() is asynchronous, returning before all files are sent.

To get your form submission to wait until all files are sent, you'll probably want to have your submit button (which should be an ordinary non-jsf button) call uploadifyUpload() ... then do nothing more. You can attach an onAllComplete() event handler to uploadify that invokes a hidden JSF2 commandLink when all the uploads are sent.

For example, add this to your uploadify ctor arguments:

'onAllComplete' : function(event,data) {
  // Escape due to css selector interpreation of colon
  // see http://docs.jquery.com/Frequently_Asked_Questions#How_do_I_select_an_element_by_an_ID_that_has_characters_used_in_CSS_notation.3F
  $('#upload\\:hiddenLink').trigger('click');
}

and use a jsf form something like this:

<h:form id="upload">
  <-- uploadify field -->
  <input id="file_upload" name="file_upload" type="file" />
  <-- Put your JSF2 input fields in here -->
  <input type="button" value="Clear Queue"  onclick="$('#file_upload').uploadifyClearQueue();"/>
  <input type="button" value="Submit Queue" onclick="$('#file_upload').uploadifyUpload();"/>
  <h:commandLink id="hiddenLink" style="display:none; visibility: hidden;"
    action="#{fileUpload.doUploadsSuccessfulAction}"
    value="doIt!"/>
</h:form>

... where String FileUpload.doUploadsSuccessfulAction() is a suitable managed bean that's ready to process the submitted form data.

1 comment:

  1. My post was written using Jersey 1.1.5, which is the version that is shipped with Glassfish 3.0.1. Jersey has gone through a lot of updates since then though, so thanks for sharing the updated code with us.

    ReplyDelete