org.springframework.web.multipart.commons.CommonsFileUploadSupport Maven / Gradle / Ivy
/*
* 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.
*
* 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.
*
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()}.
*
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.
*
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.
*
To be implemented by subclasses.
* @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.
*
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 fileItems, String encoding) {
MultiValueMap multipartFiles = new LinkedMultiValueMap<>();
Map multipartParameters = new HashMap<>();
Map 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.
* Deletes the underlying Commons FileItem instances.
* @param multipartFiles a Collection of MultipartFile instances
* @see org.apache.commons.fileupload.FileItem#delete()
*/
protected void cleanupFileItems(MultiValueMap multipartFiles) {
for (List 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 multipartFiles;
private final Map multipartParameters;
private final Map multipartParameterContentTypes;
public MultipartParsingResult(MultiValueMap mpFiles,
Map mpParams, Map mpParamContentTypes) {
this.multipartFiles = mpFiles;
this.multipartParameters = mpParams;
this.multipartParameterContentTypes = mpParamContentTypes;
}
public MultiValueMap getMultipartFiles() {
return this.multipartFiles;
}
public Map getMultipartParameters() {
return this.multipartParameters;
}
public Map getMultipartParameterContentTypes() {
return this.multipartParameterContentTypes;
}
}
}