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.support;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.Serializable;
023import java.io.UnsupportedEncodingException;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.Enumeration;
030import java.util.LinkedHashMap;
031import java.util.LinkedHashSet;
032import java.util.Map;
033import java.util.Set;
034
035import javax.mail.internet.MimeUtility;
036import javax.servlet.http.HttpServletRequest;
037import javax.servlet.http.Part;
038
039import org.springframework.http.ContentDisposition;
040import org.springframework.http.HttpHeaders;
041import org.springframework.lang.Nullable;
042import org.springframework.util.FileCopyUtils;
043import org.springframework.util.LinkedMultiValueMap;
044import org.springframework.util.MultiValueMap;
045import org.springframework.web.multipart.MaxUploadSizeExceededException;
046import org.springframework.web.multipart.MultipartException;
047import org.springframework.web.multipart.MultipartFile;
048
049/**
050 * Spring MultipartHttpServletRequest adapter, wrapping a Servlet 3.0 HttpServletRequest
051 * and its Part objects. Parameters get exposed through the native request's getParameter
052 * methods - without any custom processing on our side.
053 *
054 * @author Juergen Hoeller
055 * @author Rossen Stoyanchev
056 * @since 3.1
057 * @see StandardServletMultipartResolver
058 */
059public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
060
061        @Nullable
062        private Set<String> multipartParameterNames;
063
064
065        /**
066         * Create a new StandardMultipartHttpServletRequest wrapper for the given request,
067         * immediately parsing the multipart content.
068         * @param request the servlet request to wrap
069         * @throws MultipartException if parsing failed
070         */
071        public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
072                this(request, false);
073        }
074
075        /**
076         * Create a new StandardMultipartHttpServletRequest wrapper for the given request.
077         * @param request the servlet request to wrap
078         * @param lazyParsing whether multipart parsing should be triggered lazily on
079         * first access of multipart files or parameters
080         * @throws MultipartException if an immediate parsing attempt failed
081         * @since 3.2.9
082         */
083        public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
084                        throws MultipartException {
085
086                super(request);
087                if (!lazyParsing) {
088                        parseRequest(request);
089                }
090        }
091
092
093        private void parseRequest(HttpServletRequest request) {
094                try {
095                        Collection<Part> parts = request.getParts();
096                        this.multipartParameterNames = new LinkedHashSet<>(parts.size());
097                        MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
098                        for (Part part : parts) {
099                                String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
100                                ContentDisposition disposition = ContentDisposition.parse(headerValue);
101                                String filename = disposition.getFilename();
102                                if (filename != null) {
103                                        if (filename.startsWith("=?") && filename.endsWith("?=")) {
104                                                filename = MimeDelegate.decode(filename);
105                                        }
106                                        files.add(part.getName(), new StandardMultipartFile(part, filename));
107                                }
108                                else {
109                                        this.multipartParameterNames.add(part.getName());
110                                }
111                        }
112                        setMultipartFiles(files);
113                }
114                catch (Throwable ex) {
115                        handleParseFailure(ex);
116                }
117        }
118
119        protected void handleParseFailure(Throwable ex) {
120                String msg = ex.getMessage();
121                if (msg != null && msg.contains("size") && msg.contains("exceed")) {
122                        throw new MaxUploadSizeExceededException(-1, ex);
123                }
124                throw new MultipartException("Failed to parse multipart servlet request", ex);
125        }
126
127        @Override
128        protected void initializeMultipart() {
129                parseRequest(getRequest());
130        }
131
132        @Override
133        public Enumeration<String> getParameterNames() {
134                if (this.multipartParameterNames == null) {
135                        initializeMultipart();
136                }
137                if (this.multipartParameterNames.isEmpty()) {
138                        return super.getParameterNames();
139                }
140
141                // Servlet 3.0 getParameterNames() not guaranteed to include multipart form items
142                // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
143                Set<String> paramNames = new LinkedHashSet<>();
144                Enumeration<String> paramEnum = super.getParameterNames();
145                while (paramEnum.hasMoreElements()) {
146                        paramNames.add(paramEnum.nextElement());
147                }
148                paramNames.addAll(this.multipartParameterNames);
149                return Collections.enumeration(paramNames);
150        }
151
152        @Override
153        public Map<String, String[]> getParameterMap() {
154                if (this.multipartParameterNames == null) {
155                        initializeMultipart();
156                }
157                if (this.multipartParameterNames.isEmpty()) {
158                        return super.getParameterMap();
159                }
160
161                // Servlet 3.0 getParameterMap() not guaranteed to include multipart form items
162                // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
163                Map<String, String[]> paramMap = new LinkedHashMap<>(super.getParameterMap());
164                for (String paramName : this.multipartParameterNames) {
165                        if (!paramMap.containsKey(paramName)) {
166                                paramMap.put(paramName, getParameterValues(paramName));
167                        }
168                }
169                return paramMap;
170        }
171
172        @Override
173        public String getMultipartContentType(String paramOrFileName) {
174                try {
175                        Part part = getPart(paramOrFileName);
176                        return (part != null ? part.getContentType() : null);
177                }
178                catch (Throwable ex) {
179                        throw new MultipartException("Could not access multipart servlet request", ex);
180                }
181        }
182
183        @Override
184        public HttpHeaders getMultipartHeaders(String paramOrFileName) {
185                try {
186                        Part part = getPart(paramOrFileName);
187                        if (part != null) {
188                                HttpHeaders headers = new HttpHeaders();
189                                for (String headerName : part.getHeaderNames()) {
190                                        headers.put(headerName, new ArrayList<>(part.getHeaders(headerName)));
191                                }
192                                return headers;
193                        }
194                        else {
195                                return null;
196                        }
197                }
198                catch (Throwable ex) {
199                        throw new MultipartException("Could not access multipart servlet request", ex);
200                }
201        }
202
203
204        /**
205         * Spring MultipartFile adapter, wrapping a Servlet 3.0 Part object.
206         */
207        @SuppressWarnings("serial")
208        private static class StandardMultipartFile implements MultipartFile, Serializable {
209
210                private final Part part;
211
212                private final String filename;
213
214                public StandardMultipartFile(Part part, String filename) {
215                        this.part = part;
216                        this.filename = filename;
217                }
218
219                @Override
220                public String getName() {
221                        return this.part.getName();
222                }
223
224                @Override
225                public String getOriginalFilename() {
226                        return this.filename;
227                }
228
229                @Override
230                public String getContentType() {
231                        return this.part.getContentType();
232                }
233
234                @Override
235                public boolean isEmpty() {
236                        return (this.part.getSize() == 0);
237                }
238
239                @Override
240                public long getSize() {
241                        return this.part.getSize();
242                }
243
244                @Override
245                public byte[] getBytes() throws IOException {
246                        return FileCopyUtils.copyToByteArray(this.part.getInputStream());
247                }
248
249                @Override
250                public InputStream getInputStream() throws IOException {
251                        return this.part.getInputStream();
252                }
253
254                @Override
255                public void transferTo(File dest) throws IOException, IllegalStateException {
256                        this.part.write(dest.getPath());
257                        if (dest.isAbsolute() && !dest.exists()) {
258                                // Servlet 3.0 Part.write is not guaranteed to support absolute file paths:
259                                // may translate the given path to a relative location within a temp dir
260                                // (e.g. on Jetty whereas Tomcat and Undertow detect absolute paths).
261                                // At least we offloaded the file from memory storage; it'll get deleted
262                                // from the temp dir eventually in any case. And for our user's purposes,
263                                // we can manually copy it to the requested location as a fallback.
264                                FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));
265                        }
266                }
267
268                @Override
269                public void transferTo(Path dest) throws IOException, IllegalStateException {
270                        FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
271                }
272        }
273
274
275        /**
276         * Inner class to avoid a hard dependency on the JavaMail API.
277         */
278        private static class MimeDelegate {
279
280                public static String decode(String value) {
281                        try {
282                                return MimeUtility.decodeText(value);
283                        }
284                        catch (UnsupportedEncodingException ex) {
285                                throw new IllegalStateException(ex);
286                        }
287                }
288        }
289
290}