Wrap file upload requests

Support for file uploads was added relatively late to the Servlet Specification, starting with version 3.0. Earlier versions of the spec have poor support for file uploads. The following applies to such earlier versions.

If a form contains one or more file upload controls, the format of the underlying HTTP request changes dramatically. Unless special steps are taken, it's likely that request.getParameter(String) will return null for all parameters (both regular parameters and file upload parameters).

Reminder - if a form includes a file upload control, then it must have:

One technique for handling file upload requests uses a wrapper for the underlying request, such that request.getParameter(String) and related methods may be used in the usual way.

Example

The following wrapper uses the Apache Commons FileUpload tool to parse the request into both regular parameters and file upload parameters. An action class may use this class as it would any request, with one exception: to access the methods related specifically to files, a cast is necessary.

import java.util.*;
import java.io.*;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.FileItem;

/**
* Wrapper for a file upload request (before Servlet 3.0).
* 
* <P>This class uses the Apache Commons 
* <a href='http://commons.apache.org/fileupload/'>File Upload tool</a>.
* The generous Apache License will very likely allow you to use it in your 
* applications as well. 
*/
public class FileUploadWrapper extends HttpServletRequestWrapper {
  
  /** Constructor.  */
  public FileUploadWrapper(HttpServletRequest aRequest) throws IOException {
    super(aRequest);
    ServletFileUpload upload = new ServletFileUpload( new DiskFileItemFactory());
    try {
      List<FileItem> fileItems = upload.parseRequest(aRequest);
      convertToMaps(fileItems);
    }
    catch(FileUploadException ex){
      throw new IOException("Cannot parse underlying request: " + ex.toString());
    }
  }
  
  /**
  * Return all request parameter names, for both regular controls and file upload 
  * controls.
  */
  @Override public Enumeration<String> getParameterNames() {
    Set<String> allNames = new LinkedHashSet<>();
    allNames.addAll(fRegularParams.keySet());
    allNames.addAll(fFileParams.keySet());
    return Collections.enumeration(allNames);
  }
  
  /**
  * Return the parameter value. Applies only to regular parameters, not to 
  * file upload parameters. 
  * 
  * <P>If the parameter is not present in the underlying request, 
  * then <tt>null</tt> is returned.
  * <P>If the parameter is present, but has no  associated value, 
  * then an empty string is returned.
  * <P>If the parameter is multivalued, return the first value that 
  * appears in the request.  
  */
  @Override public String getParameter(String aName) {
    String result = null;
    List<String> values = fRegularParams.get(aName);
    if(values == null){
      //you might try the wrappee, to see if it has a value 
    }
    else if (values.isEmpty()) {
      //param name known, but no values present
      result = "";
    }
    else {
      //return first value in list
      result = values.get(FIRST_VALUE);
    }
    return result;
  }
  
  /**
  * Return the parameter values. Applies only to regular parameters, 
  * not to file upload parameters.
  */
  @Override public String[] getParameterValues(String aName) {
    String[] result = null;
    List<String> values = fRegularParams.get(aName);
    if(values != null) {
      result = values.toArray(new String[values.size()]);
    }
    return result;
  }
  
  /**
  * Return a {@code Map<String, List<String>>} for all regular parameters.
  * Does not return any file upload parameters at all. 
  */
  @Override public Map<String, List<String>> getParameterMap() {
    return Collections.unmodifiableMap(fRegularParams);
  }
  
  /**
  * Return a {@code List<FileItem>}, in the same order as they appear
  *  in the underlying request.
  */
  public List<FileItem> getFileItems(){
    return new ArrayList<FileItem>(fFileParams.values());
  }
  
  /**
  * Return the {@link FileItem} of the given name.
  * <P>If the name is unknown, then return <tt>null</tt>.
  */
  public FileItem getFileItem(String aFieldName){
    return fFileParams.get(aFieldName);
  }
  
  
  // PRIVATE
  
  /** Store regular params only. May be multivalued (hence the List).  */
  private final Map<String, List<String>> fRegularParams = new LinkedHashMap<>();

  /** Store file params only. */
  private final Map<String, FileItem> fFileParams = new LinkedHashMap<>();
  private static final int FIRST_VALUE = 0;
  
  private void convertToMaps(List<FileItem> aFileItems){
    for(FileItem item: aFileItems) {
      if ( isFileUploadField(item) ) {
        fFileParams.put(item.getFieldName(), item);
      }
      else {
        if( alreadyHasValue(item) ){
          addMultivaluedItem(item);
        }
        else {
          addSingleValueItem(item);
        }
      }
    }
  }
  
  private boolean isFileUploadField(FileItem aFileItem){
    return ! aFileItem.isFormField();
  }
  
  private boolean alreadyHasValue(FileItem aItem){
    return fRegularParams.get(aItem.getFieldName()) != null;
  }
  
  private void addSingleValueItem(FileItem aItem){
    List<String> list = new ArrayList<>();
    list.add(aItem.getString());
    fRegularParams.put(aItem.getFieldName(), list);
  }
  
  private void addMultivaluedItem(FileItem aItem){
    List<String> values = fRegularParams.get(aItem.getFieldName());
    values.add(aItem.getString());
  }
} 
The wrapper is in turn used by a Filter :
import java.io.*;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
* Filter that wraps an underlying file upload request (before Servlet 3.0). 
* 
* <P>This filter should be configured only for those operations that use a 
* file upload request.
*/
public final class FileUploadFilter implements Filter {

  public void init(FilterConfig aConfig) throws ServletException {
    //do nothing
  }
  
  public void destroy() {
    //do nothing
  }
  
  public void doFilter(
   ServletRequest aRequest, ServletResponse aResponse, FilterChain aChain
  ) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) aRequest;
    if ( isFileUploadRequest(request) ) {
      FileUploadWrapper wrapper = new FileUploadWrapper(request);
      aChain.doFilter(wrapper, aResponse);
    }
    else {
      aChain.doFilter(aRequest, aResponse);
    }
  }
  
  private boolean isFileUploadRequest(HttpServletRequest aRequest){
    return     
      aRequest.getMethod().equalsIgnoreCase("POST") && 
      aRequest.getContentType().startsWith("multipart/form-data")
    ;
  }
}
 
The Filter is configured in web.xml to handle specific URLs :

<web-app>
  <filter>
   <filter-name>FileUpload</filter-name>
   <display-name>File Upload</display-name>
   <description>
     Applied to file upload requests.
     Wraps the request, allowing it to be used as a regular request, 
     'as if' it were parsed by the Servlet API.
   </description>
   <filter-class>com.blah.FileUploadFilter</filter-class>
  </filter>

  <filter-mapping>
   <filter-name>FileUpload</filter-name>
   <url-pattern>/someurl/*</url-pattern>
  </filter-mapping>

  <!-- Required by the Apache FileUpload tool. -->
  <listener>
    <listener-class>
     org.apache.commons.fileupload.servlet.FileCleanerCleanup
    </listener-class>
  </listener> 
</web-app>

There's a variation regarding the request.getParameter(String) method which can be important here. This method takes a path argument, and that path can contain query parameters not present in the original request. Therefore, the above wrapper will not 'see' such query parameters unless special measures are taken. For example, getParameter(String) might be modified along these lines:

@Override public String getParameter(String aName) {
  String result = null;
  List values = fRegularParams.get(aName);
  if(values == null){
    //see if the 'wrappee' has a value
    String superValue = super.getParameter(aName);
    if(Util.textHasContent(superValue)) {
      result = superValue;
    }
  }
  ...elided...
}

Finally, for a second example of a file upload request wrapper, see this article by Jason Hunter.

See Also :
Understand details of FORM tags