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}