Search code examples
javawicketjquery-file-upload

Make Wicket behavior return JSON result for integration with jQuery FileUpload


In an attempt to integrate jQuery FileUpload into my Wicket project, I register an AbstractAjaxBehavior and pass its URL to the file input component such that it can be passed on to jQuery FileUpload. That is, in the constructor of a file upload panel:

AbstractAjaxBehavior fileUploadBehavior = new AbstractDefaultAjaxBehavior() {
  @Override
  protected synchronized void respond(AjaxRequestTarget target) {
    // TODO Handle incoming file(s)...
  }
};
WebMarkupContainer file = new WebMarkupContainer("file") {
  @Override
  protected void onComponentTag(ComponentTag tag) {
    super.onComponentTag(tag);

    IValueMap attributes = tag.getAttributes();
    attributes.put("data-upload-url", fileUploadBehavior.getCallbackUrl());
  }
};
add(file);
file.add(fileUploadBehavior);

The problem is that I can't seem to prevent the behavior from returning a status 302 redirect to my "stale page" error page.

The question therefore is: How do I prevent this redirect and instead return the JSON response that jQuery FileUpload expects?


Solution

  • The redirect can be suppressed by calling

    RequestCycle requestCycle = getRequestCycle();
    requestCycle.scheduleRequestHandlerAfterCurrent(null);
    

    The custom response can then be performed using

    WebResponse response = (WebResponse) requestCycle.getResponse();
    response.write(...);
    

    The uploaded files can be accessed as follows:

    ServletWebRequest request = (ServletWebRequest) requestCycle.getRequest();
    ServletFileUpload servletFileUpload = new ServletFileUpload(YourFileItemImpl::new);
    List<FileItem> fileItems = servletFileUpload.parseRequest(request.getContainerRequest());
    

    where ServletFileUpload is from the Apache Commons FileUpload library and YourFileItemImpl is some implementation of the FileItem interface from the same library. This class should at least contain appropriate implementations of the getName, getSize, and getOutputStream methods (the latter is the one doing the persistence).

    The fileItems list can now be iterated to build the appropriate response to be passed to response.write(...).

    In total, we end up with the following implementation of the behavior:

    AbstractAjaxBehavior fileUploadBehavior = new AbstractDefaultAjaxBehavior() {
      @Override
      protected synchronized void respond(AjaxRequestTarget target) {
        RequestCycle requestCycle = getRequestCycle();
    
        // Prevent default redirection.
        requestCycle.scheduleRequestHandlerAfterCurrent(null);
    
        ServletWebRequest request = (ServletWebRequest) requestCycle.getRequest();
        ServletFileUpload servletFileUpload = new ServletFileUpload(YourFileItemImpl::new);
    
        try {
          // Initialize JSON response.
          JSONObject jsonResponse = new JSONObject();
          JSONArray jsonFiles = new JSONArray();
          jsonResponse.put("files", jsonFiles);
    
          // Parse and persist uploaded file(s).
          List<FileItem> fileItems = servletFileUpload.parseRequest(request.getContainerRequest());
          // Iterate file items to build JSON response.
          for (FileItem item : fileItems) {
            JSONObject jsonFile = new JSONObject();
            jsonFiles.put(jsonFile);
    
            jsonFile.put("name", item.getName());
            jsonFile.put("size", item.getSize());
    
            // TODO Perform validation, e.g. using Apache Tika for file type detection.
            //      Add any error using `jsonFile.put("error", "[error_message]")`. Should
            //      of course take care to not persist invalid files...
         }
    
         // Write JSON response.
         WebResponse response = (WebResponse) requestCycle.getResponse();
         response.setHeader("Content-Type", "text/html; charset=utf8"); // Because IE...
         response.write(jsonResponse.toString());
       } catch (FileUploadException | IOException | JSONException e) {
         // TODO Handle exception.
       }
     }
    

    You might also want to catch the fileuploaddone event such that you can perform the proper UI updates (which you cannot do using fileUploadBehavior anymore - but you also only want to perform one UI update per file batch):

    file.add(new AjaxEventBehavior("fileuploaddone") {
      @Override
      protected void onEvent(AjaxRequestTarget target) {
        // Wait until behavior has completed.
        synchronized (fileUploadBehavior) {
          // TODO Add proper components to `target`.
        }
      }
    });
    

    Having the respond method be synchronized serves two purposes:

    1. Only accept one (possibly multifile) upload at a time. This prevents race conditions where a singleton panel end up accepting multiple files.

    2. Ensure that the fileuploaddone event is not handled before the respond callback has finished, even if it's fired before that. While this should not happen in practice, jQuery FileUpload could be buggy and this ensures robustness against that (as well as malicious users).

    Lastly, upload of multiple files can be allowed by adding

    file.add(new AttributeModifier("multiple", "multiple"));
    

    Conversely, if single-file upload is required, the behavior should validate this.