001/*
002 * Copyright 2002-2019 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.http.converter;
018
019import java.io.IOException;
020import java.io.OutputStream;
021import java.io.UnsupportedEncodingException;
022import java.net.URLDecoder;
023import java.net.URLEncoder;
024import java.nio.charset.Charset;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import javax.mail.internet.MimeUtility;
031
032import org.springframework.core.io.Resource;
033import org.springframework.http.HttpEntity;
034import org.springframework.http.HttpHeaders;
035import org.springframework.http.HttpInputMessage;
036import org.springframework.http.HttpOutputMessage;
037import org.springframework.http.MediaType;
038import org.springframework.http.StreamingHttpOutputMessage;
039import org.springframework.util.Assert;
040import org.springframework.util.LinkedMultiValueMap;
041import org.springframework.util.MimeTypeUtils;
042import org.springframework.util.MultiValueMap;
043import org.springframework.util.StreamUtils;
044import org.springframework.util.StringUtils;
045
046/**
047 * Implementation of {@link HttpMessageConverter} to read and write 'normal' HTML
048 * forms and also to write (but not read) multipart data (e.g. file uploads).
049 *
050 * <p>In other words, this converter can read and write the
051 * {@code "application/x-www-form-urlencoded"} media type as
052 * {@link MultiValueMap MultiValueMap&lt;String, String&gt;} and it can also
053 * write (but not read) the {@code "multipart/form-data"} media type as
054 * {@link MultiValueMap MultiValueMap&lt;String, Object&gt;}.
055 *
056 * <p>When writing multipart data, this converter uses other
057 * {@link HttpMessageConverter HttpMessageConverters} to write the respective
058 * MIME parts. By default, basic converters are registered (for {@code Strings}
059 * and {@code Resources}). These can be overridden through the
060 * {@link #setPartConverters partConverters} property.
061 *
062 * <p>For example, the following snippet shows how to submit an HTML form:
063 * <pre class="code">
064 * RestTemplate template = new RestTemplate();
065 * // AllEncompassingFormHttpMessageConverter is configured by default
066 *
067 * MultiValueMap&lt;String, String&gt; form = new LinkedMultiValueMap&lt;&gt;();
068 * form.add("field 1", "value 1");
069 * form.add("field 2", "value 2");
070 * form.add("field 2", "value 3");
071 * template.postForLocation("https://example.com/myForm", form);
072 * </pre>
073 *
074 * <p>The following snippet shows how to do a file upload:
075 * <pre class="code">
076 * MultiValueMap&lt;String, Object&gt; parts = new LinkedMultiValueMap&lt;&gt;();
077 * parts.add("field 1", "value 1");
078 * parts.add("file", new ClassPathResource("myFile.jpg"));
079 * template.postForLocation("https://example.com/myFileUpload", parts);
080 * </pre>
081 *
082 * <p>Some methods in this class were inspired by
083 * {@code org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity}.
084 *
085 * @author Arjen Poutsma
086 * @author Rossen Stoyanchev
087 * @author Juergen Hoeller
088 * @since 3.0
089 * @see org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
090 * @see org.springframework.util.MultiValueMap
091 */
092public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
093
094        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
095
096
097        private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
098
099        private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
100
101        private Charset charset = DEFAULT_CHARSET;
102
103        private Charset multipartCharset;
104
105
106        public FormHttpMessageConverter() {
107                this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
108                this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
109
110                StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
111                stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316
112
113                this.partConverters.add(new ByteArrayHttpMessageConverter());
114                this.partConverters.add(stringHttpMessageConverter);
115                this.partConverters.add(new ResourceHttpMessageConverter());
116
117                applyDefaultCharset();
118        }
119
120
121        /**
122         * Set the list of {@link MediaType} objects supported by this converter.
123         */
124        public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
125                this.supportedMediaTypes = supportedMediaTypes;
126        }
127
128        @Override
129        public List<MediaType> getSupportedMediaTypes() {
130                return Collections.unmodifiableList(this.supportedMediaTypes);
131        }
132
133        /**
134         * Set the message body converters to use. These converters are used to
135         * convert objects to MIME parts.
136         */
137        public void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
138                Assert.notEmpty(partConverters, "'partConverters' must not be empty");
139                this.partConverters = partConverters;
140        }
141
142        /**
143         * Add a message body converter. Such a converter is used to convert objects
144         * to MIME parts.
145         */
146        public void addPartConverter(HttpMessageConverter<?> partConverter) {
147                Assert.notNull(partConverter, "'partConverter' must not be null");
148                this.partConverters.add(partConverter);
149        }
150
151        /**
152         * Set the default character set to use for reading and writing form data when
153         * the request or response Content-Type header does not explicitly specify it.
154         * <p>By default this is set to "UTF-8". As of 4.3, it will also be used as
155         * the default charset for the conversion of text bodies in a multipart request.
156         * In contrast to this, {@link #setMultipartCharset} only affects the encoding of
157         * <i>file names</i> in a multipart request according to the encoded-word syntax.
158         */
159        public void setCharset(Charset charset) {
160                if (charset != this.charset) {
161                        this.charset = (charset != null ? charset : DEFAULT_CHARSET);
162                        applyDefaultCharset();
163                }
164        }
165
166        /**
167         * Apply the configured charset as a default to registered part converters.
168         */
169        private void applyDefaultCharset() {
170                for (HttpMessageConverter<?> candidate : this.partConverters) {
171                        if (candidate instanceof AbstractHttpMessageConverter) {
172                                AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
173                                // Only override default charset if the converter operates with a charset to begin with...
174                                if (converter.getDefaultCharset() != null) {
175                                        converter.setDefaultCharset(this.charset);
176                                }
177                        }
178                }
179        }
180
181        /**
182         * Set the character set to use when writing multipart data to encode file
183         * names. Encoding is based on the encoded-word syntax defined in RFC 2047
184         * and relies on {@code MimeUtility} from "javax.mail".
185         * <p>If not set file names will be encoded as US-ASCII.
186         * @since 4.1.1
187         * @see <a href="https://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
188         */
189        public void setMultipartCharset(Charset charset) {
190                this.multipartCharset = charset;
191        }
192
193
194        @Override
195        public boolean canRead(Class<?> clazz, MediaType mediaType) {
196                if (!MultiValueMap.class.isAssignableFrom(clazz)) {
197                        return false;
198                }
199                if (mediaType == null) {
200                        return true;
201                }
202                for (MediaType supportedMediaType : getSupportedMediaTypes()) {
203                        // We can't read multipart....
204                        if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
205                                return true;
206                        }
207                }
208                return false;
209        }
210
211        @Override
212        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
213                if (!MultiValueMap.class.isAssignableFrom(clazz)) {
214                        return false;
215                }
216                if (mediaType == null || MediaType.ALL.equals(mediaType)) {
217                        return true;
218                }
219                for (MediaType supportedMediaType : getSupportedMediaTypes()) {
220                        if (supportedMediaType.isCompatibleWith(mediaType)) {
221                                return true;
222                        }
223                }
224                return false;
225        }
226
227        @Override
228        public MultiValueMap<String, String> read(Class<? extends MultiValueMap<String, ?>> clazz,
229                        HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
230
231                MediaType contentType = inputMessage.getHeaders().getContentType();
232                Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
233                String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
234
235                String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
236                MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(pairs.length);
237                for (String pair : pairs) {
238                        int idx = pair.indexOf('=');
239                        if (idx == -1) {
240                                result.add(URLDecoder.decode(pair, charset.name()), null);
241                        }
242                        else {
243                                String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
244                                String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
245                                result.add(name, value);
246                        }
247                }
248                return result;
249        }
250
251        @Override
252        @SuppressWarnings("unchecked")
253        public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
254                        throws IOException, HttpMessageNotWritableException {
255
256                if (!isMultipart(map, contentType)) {
257                        writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
258                }
259                else {
260                        writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
261                }
262        }
263
264
265        private boolean isMultipart(MultiValueMap<String, ?> map, MediaType contentType) {
266                if (contentType != null) {
267                        return MediaType.MULTIPART_FORM_DATA.includes(contentType);
268                }
269                for (List<?> values : map.values()) {
270                        for (Object value : values) {
271                                if (value != null && !(value instanceof String)) {
272                                        return true;
273                                }
274                        }
275                }
276                return false;
277        }
278
279        private void writeForm(MultiValueMap<String, String> form, MediaType contentType,
280                        HttpOutputMessage outputMessage) throws IOException {
281
282                Charset charset;
283                if (contentType != null) {
284                        outputMessage.getHeaders().setContentType(contentType);
285                        charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
286                }
287                else {
288                        outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
289                        charset = this.charset;
290                }
291                StringBuilder builder = new StringBuilder();
292                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
293                        String name = nameIterator.next();
294                        for (Iterator<String> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
295                                String value = valueIterator.next();
296                                builder.append(URLEncoder.encode(name, charset.name()));
297                                if (value != null) {
298                                        builder.append('=');
299                                        builder.append(URLEncoder.encode(value, charset.name()));
300                                        if (valueIterator.hasNext()) {
301                                                builder.append('&');
302                                        }
303                                }
304                        }
305                        if (nameIterator.hasNext()) {
306                                builder.append('&');
307                        }
308                }
309                final byte[] bytes = builder.toString().getBytes(charset.name());
310                outputMessage.getHeaders().setContentLength(bytes.length);
311
312                if (outputMessage instanceof StreamingHttpOutputMessage) {
313                        StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
314                        streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
315                                @Override
316                                public void writeTo(OutputStream outputStream) throws IOException {
317                                        StreamUtils.copy(bytes, outputStream);
318                                }
319                        });
320                }
321                else {
322                        StreamUtils.copy(bytes, outputMessage.getBody());
323                }
324        }
325
326        private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
327                        throws IOException {
328
329                final byte[] boundary = generateMultipartBoundary();
330                Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
331
332                MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
333                HttpHeaders headers = outputMessage.getHeaders();
334                headers.setContentType(contentType);
335
336                if (outputMessage instanceof StreamingHttpOutputMessage) {
337                        StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
338                        streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
339                                @Override
340                                public void writeTo(OutputStream outputStream) throws IOException {
341                                        writeParts(outputStream, parts, boundary);
342                                        writeEnd(outputStream, boundary);
343                                }
344                        });
345                }
346                else {
347                        writeParts(outputMessage.getBody(), parts, boundary);
348                        writeEnd(outputMessage.getBody(), boundary);
349                }
350        }
351
352        private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
353                for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
354                        String name = entry.getKey();
355                        for (Object part : entry.getValue()) {
356                                if (part != null) {
357                                        writeBoundary(os, boundary);
358                                        writePart(name, getHttpEntity(part), os);
359                                        writeNewLine(os);
360                                }
361                        }
362                }
363        }
364
365        @SuppressWarnings("unchecked")
366        private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
367                Object partBody = partEntity.getBody();
368                Class<?> partType = partBody.getClass();
369                HttpHeaders partHeaders = partEntity.getHeaders();
370                MediaType partContentType = partHeaders.getContentType();
371                for (HttpMessageConverter<?> messageConverter : this.partConverters) {
372                        if (messageConverter.canWrite(partType, partContentType)) {
373                                HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
374                                multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
375                                if (!partHeaders.isEmpty()) {
376                                        multipartMessage.getHeaders().putAll(partHeaders);
377                                }
378                                ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
379                                return;
380                        }
381                }
382                throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
383                                "found for request type [" + partType.getName() + "]");
384        }
385
386        /**
387         * Generate a multipart boundary.
388         * <p>This implementation delegates to
389         * {@link MimeTypeUtils#generateMultipartBoundary()}.
390         */
391        protected byte[] generateMultipartBoundary() {
392                return MimeTypeUtils.generateMultipartBoundary();
393        }
394
395        /**
396         * Return an {@link HttpEntity} for the given part Object.
397         * @param part the part to return an {@link HttpEntity} for
398         * @return the part Object itself it is an {@link HttpEntity},
399         * or a newly built {@link HttpEntity} wrapper for that part
400         */
401        protected HttpEntity<?> getHttpEntity(Object part) {
402                return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
403        }
404
405        /**
406         * Return the filename of the given multipart part. This value will be used for the
407         * {@code Content-Disposition} header.
408         * <p>The default implementation returns {@link Resource#getFilename()} if the part is a
409         * {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses.
410         * @param part the part to determine the file name for
411         * @return the filename, or {@code null} if not known
412         */
413        protected String getFilename(Object part) {
414                if (part instanceof Resource) {
415                        Resource resource = (Resource) part;
416                        String filename = resource.getFilename();
417                        if (filename != null && this.multipartCharset != null) {
418                                filename = MimeDelegate.encode(filename, this.multipartCharset.name());
419                        }
420                        return filename;
421                }
422                else {
423                        return null;
424                }
425        }
426
427
428        private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
429                os.write('-');
430                os.write('-');
431                os.write(boundary);
432                writeNewLine(os);
433        }
434
435        private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
436                os.write('-');
437                os.write('-');
438                os.write(boundary);
439                os.write('-');
440                os.write('-');
441                writeNewLine(os);
442        }
443
444        private static void writeNewLine(OutputStream os) throws IOException {
445                os.write('\r');
446                os.write('\n');
447        }
448
449
450        /**
451         * Implementation of {@link org.springframework.http.HttpOutputMessage} used
452         * to write a MIME multipart.
453         */
454        private static class MultipartHttpOutputMessage implements HttpOutputMessage {
455
456                private final OutputStream outputStream;
457
458                private final HttpHeaders headers = new HttpHeaders();
459
460                private boolean headersWritten = false;
461
462                public MultipartHttpOutputMessage(OutputStream outputStream) {
463                        this.outputStream = outputStream;
464                }
465
466                @Override
467                public HttpHeaders getHeaders() {
468                        return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
469                }
470
471                @Override
472                public OutputStream getBody() throws IOException {
473                        writeHeaders();
474                        return this.outputStream;
475                }
476
477                private void writeHeaders() throws IOException {
478                        if (!this.headersWritten) {
479                                for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
480                                        byte[] headerName = getAsciiBytes(entry.getKey());
481                                        for (String headerValueString : entry.getValue()) {
482                                                byte[] headerValue = getAsciiBytes(headerValueString);
483                                                this.outputStream.write(headerName);
484                                                this.outputStream.write(':');
485                                                this.outputStream.write(' ');
486                                                this.outputStream.write(headerValue);
487                                                writeNewLine(this.outputStream);
488                                        }
489                                }
490                                writeNewLine(this.outputStream);
491                                this.headersWritten = true;
492                        }
493                }
494
495                private byte[] getAsciiBytes(String name) {
496                        try {
497                                return name.getBytes("US-ASCII");
498                        }
499                        catch (UnsupportedEncodingException ex) {
500                                // Should not happen - US-ASCII is always supported.
501                                throw new IllegalStateException(ex);
502                        }
503                }
504        }
505
506
507        /**
508         * Inner class to avoid a hard dependency on the JavaMail API.
509         */
510        private static class MimeDelegate {
511
512                public static String encode(String value, String charset) {
513                        try {
514                                return MimeUtility.encodeText(value, charset, null);
515                        }
516                        catch (UnsupportedEncodingException ex) {
517                                throw new IllegalStateException(ex);
518                        }
519                }
520        }
521
522}