Open Source Repository

Home /spring/spring-web-3.0.5 | Repository Home


org/springframework/web/multipart/commons/CommonsFileUploadSupport.java
/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.multipart.commons;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.WebUtils;

/**
 * Base class for multipart resolvers that use Jakarta Commons FileUpload
 * 1.2 or above.
 *
 <p>Provides common configuration properties and parsing functionality
 * for multipart requests, using a Map of Spring CommonsMultipartFile instances
 * as representation of uploaded files and a String-based parameter Map as
 * representation of uploaded form fields.
 *
 <p>Subclasses implement concrete resolution strategies for Servlet or Portlet
 * environments: see CommonsMultipartResolver and CommonsPortletMultipartResolver,
 * respectively. This base class is not tied to either of those APIs, factoring
 * out common functionality.
 *
 @author Juergen Hoeller
 @since 2.0
 @see CommonsMultipartFile
 @see CommonsMultipartResolver
 @see org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver
 */
public abstract class CommonsFileUploadSupport {

  protected final Log logger = LogFactory.getLog(getClass());

  private final DiskFileItemFactory fileItemFactory;

  private final FileUpload fileUpload;

  private boolean uploadTempDirSpecified = false;


  /**
   * Instantiate a new CommonsFileUploadSupport with its
   * corresponding FileItemFactory and FileUpload instances.
   @see #newFileItemFactory
   @see #newFileUpload
   */
  public CommonsFileUploadSupport() {
    this.fileItemFactory = newFileItemFactory();
    this.fileUpload = newFileUpload(getFileItemFactory());
  }


  /**
   * Return the underlying <code>org.apache.commons.fileupload.disk.DiskFileItemFactory</code>
   * instance. There is hardly any need to access this.
   @return the underlying DiskFileItemFactory instance
   */
  public DiskFileItemFactory getFileItemFactory() {
    return this.fileItemFactory;
  }

  /**
   * Return the underlying <code>org.apache.commons.fileupload.FileUpload</code>
   * instance. There is hardly any need to access this.
   @return the underlying FileUpload instance
   */
  public FileUpload getFileUpload() {
    return this.fileUpload;
  }

  /**
   * Set the maximum allowed size (in bytes) before uploads are refused.
   * -1 indicates no limit (the default).
   @param maxUploadSize the maximum upload size allowed
   @see org.apache.commons.fileupload.FileUploadBase#setSizeMax
   */
  public void setMaxUploadSize(long maxUploadSize) {
    this.fileUpload.setSizeMax(maxUploadSize);
  }

  /**
   * Set the maximum allowed size (in bytes) before uploads are written to disk.
   * Uploaded files will still be received past this amount, but they will not be
   * stored in memory. Default is 10240, according to Commons FileUpload.
   @param maxInMemorySize the maximum in memory size allowed
   @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold
   */
  public void setMaxInMemorySize(int maxInMemorySize) {
    this.fileItemFactory.setSizeThreshold(maxInMemorySize);
  }

  /**
   * Set the default character encoding to use for parsing requests,
   * to be applied to headers of individual parts and to form fields.
   * Default is ISO-8859-1, according to the Servlet spec.
   <p>If the request specifies a character encoding itself, the request
   * encoding will override this setting. This also allows for generically
   * overriding the character encoding in a filter that invokes the
   <code>ServletRequest.setCharacterEncoding</code> method.
   @param defaultEncoding the character encoding to use
   @see javax.servlet.ServletRequest#getCharacterEncoding
   @see javax.servlet.ServletRequest#setCharacterEncoding
   @see WebUtils#DEFAULT_CHARACTER_ENCODING
   @see org.apache.commons.fileupload.FileUploadBase#setHeaderEncoding
   */
  public void setDefaultEncoding(String defaultEncoding) {
    this.fileUpload.setHeaderEncoding(defaultEncoding);
  }

  protected String getDefaultEncoding() {
    String encoding = getFileUpload().getHeaderEncoding();
    if (encoding == null) {
      encoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
    }
    return encoding;
  }

  /**
   * Set the temporary directory where uploaded files get stored.
   * Default is the servlet container's temporary directory for the web application.
   @see org.springframework.web.util.WebUtils#TEMP_DIR_CONTEXT_ATTRIBUTE
   */
  public void setUploadTempDir(Resource uploadTempDirthrows IOException {
    if (!uploadTempDir.exists() && !uploadTempDir.getFile().mkdirs()) {
      throw new IllegalArgumentException("Given uploadTempDir [" + uploadTempDir + "] could not be created");
    }
    this.fileItemFactory.setRepository(uploadTempDir.getFile());
    this.uploadTempDirSpecified = true;
  }

  protected boolean isUploadTempDirSpecified() {
    return this.uploadTempDirSpecified;
  }


  /**
   * Factory method for a Commons DiskFileItemFactory instance.
   <p>Default implementation returns a standard DiskFileItemFactory.
   * Can be overridden to use a custom subclass, e.g. for testing purposes.
   @return the new DiskFileItemFactory instance
   */
  protected DiskFileItemFactory newFileItemFactory() {
    return new DiskFileItemFactory();
  }

  /**
   * Factory method for a Commons FileUpload instance.
   <p><b>To be implemented by subclasses.</b>
   @param fileItemFactory the Commons FileItemFactory to build upon
   @return the Commons FileUpload instance
   */
  protected abstract FileUpload newFileUpload(FileItemFactory fileItemFactory);


  /**
   * Determine an appropriate FileUpload instance for the given encoding.
   <p>Default implementation returns the shared FileUpload instance
   * if the encoding matches, else creates a new FileUpload instance
   * with the same configuration other than the desired encoding.
   @param encoding the character encoding to use
   @return an appropriate FileUpload instance.
   */
  protected FileUpload prepareFileUpload(String encoding) {
    FileUpload fileUpload = getFileUpload();
    FileUpload actualFileUpload = fileUpload;

    // Use new temporary FileUpload instance if the request specifies
    // its own encoding that does not match the default encoding.
    if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) {
      actualFileUpload = newFileUpload(getFileItemFactory());
      actualFileUpload.setSizeMax(fileUpload.getSizeMax());
      actualFileUpload.setHeaderEncoding(encoding);
    }

    return actualFileUpload;
  }

  /**
   * Parse the given List of Commons FileItems into a Spring MultipartParsingResult,
   * containing Spring MultipartFile instances and a Map of multipart parameter.
   @param fileItems the Commons FileIterms to parse
   @param encoding the encoding to use for form fields
   @return the Spring MultipartParsingResult
   @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem)
   */
  protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
    MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>();
    Map<String, String[]> multipartParameters = new HashMap<String, String[]>();

    // Extract multipart files and multipart parameters.
    for (FileItem fileItem : fileItems) {
      if (fileItem.isFormField()) {
        String value;
        String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
        if (partEncoding != null) {
          try {
            value = fileItem.getString(partEncoding);
          }
          catch (UnsupportedEncodingException ex) {
            if (logger.isWarnEnabled()) {
              logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
                  "' with encoding '" + partEncoding + "': using platform default");
            }
            value = fileItem.getString();
          }
        }
        else {
          value = fileItem.getString();
        }
        String[] curParam = multipartParameters.get(fileItem.getFieldName());
        if (curParam == null) {
          // simple form field
          multipartParameters.put(fileItem.getFieldName()new String[] {value});
        }
        else {
          // array of simple form fields
          String[] newParam = StringUtils.addStringToArray(curParam, value);
          multipartParameters.put(fileItem.getFieldName(), newParam);
        }
      }
      else {
        // multipart file field
        CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
        multipartFiles.add(file.getName(), file);
        if (logger.isDebugEnabled()) {
          logger.debug("Found multipart file [" + file.getName() "] of size " + file.getSize() +
              " bytes with original filename [" + file.getOriginalFilename() "], stored " +
              file.getStorageDescription());
        }
      }
    }
    return new MultipartParsingResult(multipartFiles, multipartParameters);
  }

  /**
   * Cleanup the Spring MultipartFiles created during multipart parsing,
   * potentially holding temporary data on disk.
   <p>Deletes the underlying Commons FileItem instances.
   @param multipartFiles Collection of MultipartFile instances
   @see org.apache.commons.fileupload.FileItem#delete()
   */
  protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {
    for (List<MultipartFile> files : multipartFiles.values()) {
      for (MultipartFile file : files) {
        if (file instanceof CommonsMultipartFile) {
          CommonsMultipartFile cmf = (CommonsMultipartFilefile;
          cmf.getFileItem().delete();
          if (logger.isDebugEnabled()) {
            logger.debug("Cleaning up multipart file [" + cmf.getName() "] with original filename [" +
                cmf.getOriginalFilename() "], stored " + cmf.getStorageDescription());
          }
        }
      }
    }
  }

  private String determineEncoding(String contentTypeHeader, String defaultEncoding) {
    if (!StringUtils.hasText(contentTypeHeader)) {
      return defaultEncoding;
    }
    MediaType contentType = MediaType.parseMediaType(contentTypeHeader);
    Charset charset = contentType.getCharSet();
    return charset != null ? charset.name() : defaultEncoding;
  }


  /**
   * Holder for a Map of Spring MultipartFiles and a Map of
   * multipart parameters.
   */
  protected static class MultipartParsingResult {

    private final MultiValueMap<String, MultipartFile> multipartFiles;

    private final Map<String, String[]> multipartParameters;

    /**
     * Create a new MultipartParsingResult.
     @param mpFiles Map of field name to MultipartFile instance
     @param mpParams Map of field name to form field String value
     */
    public MultipartParsingResult(MultiValueMap<String, MultipartFile> mpFiles, Map<String, String[]> mpParams) {
      this.multipartFiles = mpFiles;
      this.multipartParameters = mpParams;
    }

    /**
     * Return the multipart files as Map of field name to MultipartFile instance.
     */
    public MultiValueMap<String, MultipartFile> getMultipartFiles() {
      return this.multipartFiles;
    }

    /**
     * Return the multipart parameters as Map of field name to form field String value.
     */
    public Map<String, String[]> getMultipartParameters() {
      return this.multipartParameters;
    }
  }

}