001/* 002 * Copyright 2002-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.multipart.commons; 018 019import java.io.IOException; 020import java.io.UnsupportedEncodingException; 021import java.nio.charset.Charset; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.commons.fileupload.FileItem; 027import org.apache.commons.fileupload.FileItemFactory; 028import org.apache.commons.fileupload.FileUpload; 029import org.apache.commons.fileupload.disk.DiskFileItemFactory; 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032 033import org.springframework.core.io.Resource; 034import org.springframework.http.MediaType; 035import org.springframework.util.LinkedMultiValueMap; 036import org.springframework.util.MultiValueMap; 037import org.springframework.util.StringUtils; 038import org.springframework.web.multipart.MultipartFile; 039import org.springframework.web.util.WebUtils; 040 041/** 042 * Base class for multipart resolvers that use Apache Commons FileUpload 043 * 1.2 or above. 044 * 045 * <p>Provides common configuration properties and parsing functionality 046 * for multipart requests, using a Map of Spring CommonsMultipartFile instances 047 * as representation of uploaded files and a String-based parameter Map as 048 * representation of uploaded form fields. 049 * 050 * <p>Subclasses implement concrete resolution strategies for Servlet or Portlet 051 * environments: see CommonsMultipartResolver and CommonsPortletMultipartResolver, 052 * respectively. This base class is not tied to either of those APIs, factoring 053 * out common functionality. 054 * 055 * @author Juergen Hoeller 056 * @since 2.0 057 * @see CommonsMultipartFile 058 * @see CommonsMultipartResolver 059 * @see org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver 060 */ 061public abstract class CommonsFileUploadSupport { 062 063 protected final Log logger = LogFactory.getLog(getClass()); 064 065 private final DiskFileItemFactory fileItemFactory; 066 067 private final FileUpload fileUpload; 068 069 private boolean uploadTempDirSpecified = false; 070 071 private boolean preserveFilename = false; 072 073 074 /** 075 * Instantiate a new CommonsFileUploadSupport with its 076 * corresponding FileItemFactory and FileUpload instances. 077 * @see #newFileItemFactory 078 * @see #newFileUpload 079 */ 080 public CommonsFileUploadSupport() { 081 this.fileItemFactory = newFileItemFactory(); 082 this.fileUpload = newFileUpload(getFileItemFactory()); 083 } 084 085 086 /** 087 * Return the underlying {@code org.apache.commons.fileupload.disk.DiskFileItemFactory} 088 * instance. There is hardly any need to access this. 089 * @return the underlying DiskFileItemFactory instance 090 */ 091 public DiskFileItemFactory getFileItemFactory() { 092 return this.fileItemFactory; 093 } 094 095 /** 096 * Return the underlying {@code org.apache.commons.fileupload.FileUpload} 097 * instance. There is hardly any need to access this. 098 * @return the underlying FileUpload instance 099 */ 100 public FileUpload getFileUpload() { 101 return this.fileUpload; 102 } 103 104 /** 105 * Set the maximum allowed size (in bytes) before an upload gets rejected. 106 * -1 indicates no limit (the default). 107 * @param maxUploadSize the maximum upload size allowed 108 * @see org.apache.commons.fileupload.FileUploadBase#setSizeMax 109 */ 110 public void setMaxUploadSize(long maxUploadSize) { 111 this.fileUpload.setSizeMax(maxUploadSize); 112 } 113 114 /** 115 * Set the maximum allowed size (in bytes) for each individual file before 116 * an upload gets rejected. -1 indicates no limit (the default). 117 * @param maxUploadSizePerFile the maximum upload size per file 118 * @since 4.2 119 * @see org.apache.commons.fileupload.FileUploadBase#setFileSizeMax 120 */ 121 public void setMaxUploadSizePerFile(long maxUploadSizePerFile) { 122 this.fileUpload.setFileSizeMax(maxUploadSizePerFile); 123 } 124 125 /** 126 * Set the maximum allowed size (in bytes) before uploads are written to disk. 127 * Uploaded files will still be received past this amount, but they will not be 128 * stored in memory. Default is 10240, according to Commons FileUpload. 129 * @param maxInMemorySize the maximum in memory size allowed 130 * @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold 131 */ 132 public void setMaxInMemorySize(int maxInMemorySize) { 133 this.fileItemFactory.setSizeThreshold(maxInMemorySize); 134 } 135 136 /** 137 * Set the default character encoding to use for parsing requests, 138 * to be applied to headers of individual parts and to form fields. 139 * Default is ISO-8859-1, according to the Servlet spec. 140 * <p>If the request specifies a character encoding itself, the request 141 * encoding will override this setting. This also allows for generically 142 * overriding the character encoding in a filter that invokes the 143 * {@code ServletRequest.setCharacterEncoding} method. 144 * @param defaultEncoding the character encoding to use 145 * @see javax.servlet.ServletRequest#getCharacterEncoding 146 * @see javax.servlet.ServletRequest#setCharacterEncoding 147 * @see WebUtils#DEFAULT_CHARACTER_ENCODING 148 * @see org.apache.commons.fileupload.FileUploadBase#setHeaderEncoding 149 */ 150 public void setDefaultEncoding(String defaultEncoding) { 151 this.fileUpload.setHeaderEncoding(defaultEncoding); 152 } 153 154 /** 155 * Determine the default encoding to use for parsing requests. 156 * @see #setDefaultEncoding 157 */ 158 protected String getDefaultEncoding() { 159 String encoding = getFileUpload().getHeaderEncoding(); 160 if (encoding == null) { 161 encoding = WebUtils.DEFAULT_CHARACTER_ENCODING; 162 } 163 return encoding; 164 } 165 166 /** 167 * Set the temporary directory where uploaded files get stored. 168 * Default is the servlet container's temporary directory for the web application. 169 * @see org.springframework.web.util.WebUtils#TEMP_DIR_CONTEXT_ATTRIBUTE 170 */ 171 public void setUploadTempDir(Resource uploadTempDir) throws IOException { 172 if (!uploadTempDir.exists() && !uploadTempDir.getFile().mkdirs()) { 173 throw new IllegalArgumentException("Given uploadTempDir [" + uploadTempDir + "] could not be created"); 174 } 175 this.fileItemFactory.setRepository(uploadTempDir.getFile()); 176 this.uploadTempDirSpecified = true; 177 } 178 179 /** 180 * Return the temporary directory where uploaded files get stored. 181 * @see #setUploadTempDir 182 */ 183 protected boolean isUploadTempDirSpecified() { 184 return this.uploadTempDirSpecified; 185 } 186 187 /** 188 * Set whether to preserve the filename as sent by the client, not stripping off 189 * path information in {@link CommonsMultipartFile#getOriginalFilename()}. 190 * <p>Default is "false", stripping off path information that may prefix the 191 * actual filename e.g. from Opera. Switch this to "true" for preserving the 192 * client-specified filename as-is, including potential path separators. 193 * @since 4.3.5 194 * @see MultipartFile#getOriginalFilename() 195 * @see CommonsMultipartFile#setPreserveFilename(boolean) 196 */ 197 public void setPreserveFilename(boolean preserveFilename) { 198 this.preserveFilename = preserveFilename; 199 } 200 201 202 /** 203 * Factory method for a Commons DiskFileItemFactory instance. 204 * <p>Default implementation returns a standard DiskFileItemFactory. 205 * Can be overridden to use a custom subclass, e.g. for testing purposes. 206 * @return the new DiskFileItemFactory instance 207 */ 208 protected DiskFileItemFactory newFileItemFactory() { 209 return new DiskFileItemFactory(); 210 } 211 212 /** 213 * Factory method for a Commons FileUpload instance. 214 * <p><b>To be implemented by subclasses.</b> 215 * @param fileItemFactory the Commons FileItemFactory to build upon 216 * @return the Commons FileUpload instance 217 */ 218 protected abstract FileUpload newFileUpload(FileItemFactory fileItemFactory); 219 220 221 /** 222 * Determine an appropriate FileUpload instance for the given encoding. 223 * <p>Default implementation returns the shared FileUpload instance 224 * if the encoding matches, else creates a new FileUpload instance 225 * with the same configuration other than the desired encoding. 226 * @param encoding the character encoding to use 227 * @return an appropriate FileUpload instance. 228 */ 229 protected FileUpload prepareFileUpload(String encoding) { 230 FileUpload fileUpload = getFileUpload(); 231 FileUpload actualFileUpload = fileUpload; 232 233 // Use new temporary FileUpload instance if the request specifies 234 // its own encoding that does not match the default encoding. 235 if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) { 236 actualFileUpload = newFileUpload(getFileItemFactory()); 237 actualFileUpload.setSizeMax(fileUpload.getSizeMax()); 238 actualFileUpload.setFileSizeMax(fileUpload.getFileSizeMax()); 239 actualFileUpload.setHeaderEncoding(encoding); 240 } 241 242 return actualFileUpload; 243 } 244 245 /** 246 * Parse the given List of Commons FileItems into a Spring MultipartParsingResult, 247 * containing Spring MultipartFile instances and a Map of multipart parameter. 248 * @param fileItems the Commons FileIterms to parse 249 * @param encoding the encoding to use for form fields 250 * @return the Spring MultipartParsingResult 251 * @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem) 252 */ 253 protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) { 254 MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>(); 255 Map<String, String[]> multipartParameters = new HashMap<String, String[]>(); 256 Map<String, String> multipartParameterContentTypes = new HashMap<String, String>(); 257 258 // Extract multipart files and multipart parameters. 259 for (FileItem fileItem : fileItems) { 260 if (fileItem.isFormField()) { 261 String value; 262 String partEncoding = determineEncoding(fileItem.getContentType(), encoding); 263 if (partEncoding != null) { 264 try { 265 value = fileItem.getString(partEncoding); 266 } 267 catch (UnsupportedEncodingException ex) { 268 if (logger.isWarnEnabled()) { 269 logger.warn("Could not decode multipart item '" + fileItem.getFieldName() + 270 "' with encoding '" + partEncoding + "': using platform default"); 271 } 272 value = fileItem.getString(); 273 } 274 } 275 else { 276 value = fileItem.getString(); 277 } 278 String[] curParam = multipartParameters.get(fileItem.getFieldName()); 279 if (curParam == null) { 280 // simple form field 281 multipartParameters.put(fileItem.getFieldName(), new String[] {value}); 282 } 283 else { 284 // array of simple form fields 285 String[] newParam = StringUtils.addStringToArray(curParam, value); 286 multipartParameters.put(fileItem.getFieldName(), newParam); 287 } 288 multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType()); 289 } 290 else { 291 // multipart file field 292 CommonsMultipartFile file = createMultipartFile(fileItem); 293 multipartFiles.add(file.getName(), file); 294 if (logger.isDebugEnabled()) { 295 logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() + 296 " bytes with original filename [" + file.getOriginalFilename() + "], stored " + 297 file.getStorageDescription()); 298 } 299 } 300 } 301 return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes); 302 } 303 304 /** 305 * Create a {@link CommonsMultipartFile} wrapper for the given Commons {@link FileItem}. 306 * @param fileItem the Commons FileItem to wrap 307 * @return the corresponding CommonsMultipartFile (potentially a custom subclass) 308 * @since 4.3.5 309 * @see #setPreserveFilename(boolean) 310 * @see CommonsMultipartFile#setPreserveFilename(boolean) 311 */ 312 protected CommonsMultipartFile createMultipartFile(FileItem fileItem) { 313 CommonsMultipartFile multipartFile = new CommonsMultipartFile(fileItem); 314 multipartFile.setPreserveFilename(this.preserveFilename); 315 return multipartFile; 316 } 317 318 /** 319 * Cleanup the Spring MultipartFiles created during multipart parsing, 320 * potentially holding temporary data on disk. 321 * <p>Deletes the underlying Commons FileItem instances. 322 * @param multipartFiles Collection of MultipartFile instances 323 * @see org.apache.commons.fileupload.FileItem#delete() 324 */ 325 protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) { 326 for (List<MultipartFile> files : multipartFiles.values()) { 327 for (MultipartFile file : files) { 328 if (file instanceof CommonsMultipartFile) { 329 CommonsMultipartFile cmf = (CommonsMultipartFile) file; 330 cmf.getFileItem().delete(); 331 if (logger.isDebugEnabled()) { 332 logger.debug("Cleaning up multipart file [" + cmf.getName() + "] with original filename [" + 333 cmf.getOriginalFilename() + "], stored " + cmf.getStorageDescription()); 334 } 335 } 336 } 337 } 338 } 339 340 private String determineEncoding(String contentTypeHeader, String defaultEncoding) { 341 if (!StringUtils.hasText(contentTypeHeader)) { 342 return defaultEncoding; 343 } 344 MediaType contentType = MediaType.parseMediaType(contentTypeHeader); 345 Charset charset = contentType.getCharset(); 346 return (charset != null ? charset.name() : defaultEncoding); 347 } 348 349 350 /** 351 * Holder for a Map of Spring MultipartFiles and a Map of 352 * multipart parameters. 353 */ 354 protected static class MultipartParsingResult { 355 356 private final MultiValueMap<String, MultipartFile> multipartFiles; 357 358 private final Map<String, String[]> multipartParameters; 359 360 private final Map<String, String> multipartParameterContentTypes; 361 362 public MultipartParsingResult(MultiValueMap<String, MultipartFile> mpFiles, 363 Map<String, String[]> mpParams, Map<String, String> mpParamContentTypes) { 364 365 this.multipartFiles = mpFiles; 366 this.multipartParameters = mpParams; 367 this.multipartParameterContentTypes = mpParamContentTypes; 368 } 369 370 public MultiValueMap<String, MultipartFile> getMultipartFiles() { 371 return this.multipartFiles; 372 } 373 374 public Map<String, String[]> getMultipartParameters() { 375 return this.multipartParameters; 376 } 377 378 public Map<String, String> getMultipartParameterContentTypes() { 379 return this.multipartParameterContentTypes; 380 } 381 } 382 383}