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}