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