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}