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.codec;
018
019import java.io.UnsupportedEncodingException;
020import java.net.URLDecoder;
021import java.nio.CharBuffer;
022import java.nio.charset.Charset;
023import java.nio.charset.StandardCharsets;
024import java.util.Collections;
025import java.util.List;
026import java.util.Map;
027
028import reactor.core.publisher.Flux;
029import reactor.core.publisher.Mono;
030
031import org.springframework.core.ResolvableType;
032import org.springframework.core.codec.Hints;
033import org.springframework.core.io.buffer.DataBufferLimitException;
034import org.springframework.core.io.buffer.DataBufferUtils;
035import org.springframework.core.log.LogFormatUtils;
036import org.springframework.http.MediaType;
037import org.springframework.http.ReactiveHttpInputMessage;
038import org.springframework.lang.Nullable;
039import org.springframework.util.Assert;
040import org.springframework.util.LinkedMultiValueMap;
041import org.springframework.util.MultiValueMap;
042import org.springframework.util.StringUtils;
043
044/**
045 * Implementation of an {@link HttpMessageReader} to read HTML form data, i.e.
046 * request body with media type {@code "application/x-www-form-urlencoded"}.
047 *
048 * @author Sebastien Deleuze
049 * @author Rossen Stoyanchev
050 * @since 5.0
051 */
052public class FormHttpMessageReader extends LoggingCodecSupport
053                implements HttpMessageReader<MultiValueMap<String, String>> {
054
055        /**
056         * The default charset used by the reader.
057         */
058        public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
059
060        private static final ResolvableType MULTIVALUE_STRINGS_TYPE =
061                        ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
062
063
064        private Charset defaultCharset = DEFAULT_CHARSET;
065
066        private int maxInMemorySize = 256 * 1024;
067
068
069        /**
070         * Set the default character set to use for reading form data when the
071         * request Content-Type header does not explicitly specify it.
072         * <p>By default this is set to "UTF-8".
073         */
074        public void setDefaultCharset(Charset charset) {
075                Assert.notNull(charset, "Charset must not be null");
076                this.defaultCharset = charset;
077        }
078
079        /**
080         * Return the configured default charset.
081         */
082        public Charset getDefaultCharset() {
083                return this.defaultCharset;
084        }
085
086        /**
087         * Set the max number of bytes for input form data. As form data is buffered
088         * before it is parsed, this helps to limit the amount of buffering. Once
089         * the limit is exceeded, {@link DataBufferLimitException} is raised.
090         * <p>By default this is set to 256K.
091         * @param byteCount the max number of bytes to buffer, or -1 for unlimited
092         * @since 5.1.11
093         */
094        public void setMaxInMemorySize(int byteCount) {
095                this.maxInMemorySize = byteCount;
096        }
097
098        /**
099         * Return the {@link #setMaxInMemorySize configured} byte count limit.
100         * @since 5.1.11
101         */
102        public int getMaxInMemorySize() {
103                return this.maxInMemorySize;
104        }
105
106
107        @Override
108        public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
109                boolean multiValueUnresolved =
110                                elementType.hasUnresolvableGenerics() &&
111                                                MultiValueMap.class.isAssignableFrom(elementType.toClass());
112
113                return ((MULTIVALUE_STRINGS_TYPE.isAssignableFrom(elementType) || multiValueUnresolved) &&
114                                (mediaType == null || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)));
115        }
116
117        @Override
118        public Flux<MultiValueMap<String, String>> read(ResolvableType elementType,
119                        ReactiveHttpInputMessage message, Map<String, Object> hints) {
120
121                return Flux.from(readMono(elementType, message, hints));
122        }
123
124        @Override
125        public Mono<MultiValueMap<String, String>> readMono(ResolvableType elementType,
126                        ReactiveHttpInputMessage message, Map<String, Object> hints) {
127
128                MediaType contentType = message.getHeaders().getContentType();
129                Charset charset = getMediaTypeCharset(contentType);
130
131                return DataBufferUtils.join(message.getBody(), this.maxInMemorySize)
132                                .map(buffer -> {
133                                        CharBuffer charBuffer = charset.decode(buffer.asByteBuffer());
134                                        String body = charBuffer.toString();
135                                        DataBufferUtils.release(buffer);
136                                        MultiValueMap<String, String> formData = parseFormData(charset, body);
137                                        logFormData(formData, hints);
138                                        return formData;
139                                });
140        }
141
142        private void logFormData(MultiValueMap<String, String> formData, Map<String, Object> hints) {
143                LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Read " +
144                                (isEnableLoggingRequestDetails() ?
145                                                LogFormatUtils.formatValue(formData, !traceOn) :
146                                                "form fields " + formData.keySet() + " (content masked)"));
147        }
148
149        private Charset getMediaTypeCharset(@Nullable MediaType mediaType) {
150                if (mediaType != null && mediaType.getCharset() != null) {
151                        return mediaType.getCharset();
152                }
153                else {
154                        return getDefaultCharset();
155                }
156        }
157
158        private MultiValueMap<String, String> parseFormData(Charset charset, String body) {
159                String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
160                MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
161                try {
162                        for (String pair : pairs) {
163                                int idx = pair.indexOf('=');
164                                if (idx == -1) {
165                                        result.add(URLDecoder.decode(pair, charset.name()), null);
166                                }
167                                else {
168                                        String name = URLDecoder.decode(pair.substring(0, idx),  charset.name());
169                                        String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
170                                        result.add(name, value);
171                                }
172                        }
173                }
174                catch (UnsupportedEncodingException ex) {
175                        throw new IllegalStateException(ex);
176                }
177                return result;
178        }
179
180        @Override
181        public List<MediaType> getReadableMediaTypes() {
182                return Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED);
183        }
184
185}