org.springframework.web.multipart.commons.CommonsFileUploadSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.web.multipart.commons.CommonsFileUploadSupport.java

Source

/*
 * Copyright 2002-2018 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
 *
 *      https://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.core.log.LogFormatUtils;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
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 Apache 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.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see CommonsMultipartFile
 * @see CommonsMultipartResolver
 */
public abstract class CommonsFileUploadSupport {

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

    private final DiskFileItemFactory fileItemFactory;

    private final FileUpload fileUpload;

    private boolean uploadTempDirSpecified = false;

    private boolean preserveFilename = 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}
     * 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}
     * 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 an upload gets rejected.
     * -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) for each individual file before
     * an upload gets rejected. -1 indicates no limit (the default).
     * @param maxUploadSizePerFile the maximum upload size per file
     * @since 4.2
     * @see org.apache.commons.fileupload.FileUploadBase#setFileSizeMax
     */
    public void setMaxUploadSizePerFile(long maxUploadSizePerFile) {
        this.fileUpload.setFileSizeMax(maxUploadSizePerFile);
    }

    /**
     * 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} 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);
    }

    /**
     * Determine the default encoding to use for parsing requests.
     * @see #setDefaultEncoding
     */
    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 uploadTempDir) throws 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;
    }

    /**
     * Return the temporary directory where uploaded files get stored.
     * @see #setUploadTempDir
     */
    protected boolean isUploadTempDirSpecified() {
        return this.uploadTempDirSpecified;
    }

    /**
     * Set whether to preserve the filename as sent by the client, not stripping off
     * path information in {@link CommonsMultipartFile#getOriginalFilename()}.
     * <p>Default is "false", stripping off path information that may prefix the
     * actual filename e.g. from Opera. Switch this to "true" for preserving the
     * client-specified filename as-is, including potential path separators.
     * @since 4.3.5
     * @see MultipartFile#getOriginalFilename()
     * @see CommonsMultipartFile#setPreserveFilename(boolean)
     */
    public void setPreserveFilename(boolean preserveFilename) {
        this.preserveFilename = preserveFilename;
    }

    /**
     * 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(@Nullable 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.setFileSizeMax(fileUpload.getFileSizeMax());
            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 FileItems 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<>();
        Map<String, String[]> multipartParameters = new HashMap<>();
        Map<String, String> multipartParameterContentTypes = new HashMap<>();

        // Extract multipart files and multipart parameters.
        for (FileItem fileItem : fileItems) {
            if (fileItem.isFormField()) {
                String value;
                String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
                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();
                }
                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);
                }
                multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
            } else {
                // multipart file field
                CommonsMultipartFile file = createMultipartFile(fileItem);
                multipartFiles.add(file.getName(), file);
                LogFormatUtils.traceDebug(logger,
                        traceOn -> "Part '" + file.getName() + "', size " + file.getSize() + " bytes, filename='"
                                + file.getOriginalFilename() + "'"
                                + (traceOn ? ", storage=" + file.getStorageDescription() : ""));
            }
        }
        return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
    }

    /**
     * Create a {@link CommonsMultipartFile} wrapper for the given Commons {@link FileItem}.
     * @param fileItem the Commons FileItem to wrap
     * @return the corresponding CommonsMultipartFile (potentially a custom subclass)
     * @since 4.3.5
     * @see #setPreserveFilename(boolean)
     * @see CommonsMultipartFile#setPreserveFilename(boolean)
     */
    protected CommonsMultipartFile createMultipartFile(FileItem fileItem) {
        CommonsMultipartFile multipartFile = new CommonsMultipartFile(fileItem);
        multipartFile.setPreserveFilename(this.preserveFilename);
        return multipartFile;
    }

    /**
     * Cleanup the Spring MultipartFiles created during multipart parsing,
     * potentially holding temporary data on disk.
     * <p>Deletes the underlying Commons FileItem instances.
     * @param multipartFiles a 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 = (CommonsMultipartFile) file;
                    cmf.getFileItem().delete();
                    LogFormatUtils.traceDebug(logger,
                            traceOn -> "Cleaning up part '" + cmf.getName() + "', filename '"
                                    + cmf.getOriginalFilename() + "'"
                                    + (traceOn ? ", 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;

        private final Map<String, String> multipartParameterContentTypes;

        public MultipartParsingResult(MultiValueMap<String, MultipartFile> mpFiles, Map<String, String[]> mpParams,
                Map<String, String> mpParamContentTypes) {

            this.multipartFiles = mpFiles;
            this.multipartParameters = mpParams;
            this.multipartParameterContentTypes = mpParamContentTypes;
        }

        public MultiValueMap<String, MultipartFile> getMultipartFiles() {
            return this.multipartFiles;
        }

        public Map<String, String[]> getMultipartParameters() {
            return this.multipartParameters;
        }

        public Map<String, String> getMultipartParameterContentTypes() {
            return this.multipartParameterContentTypes;
        }
    }

}