Skip to content

Commit 81b4ded

Browse files
committed
Polish form reader/writer
1 parent 46599e7 commit 81b4ded

File tree

4 files changed

+144
-86
lines changed

4 files changed

+144
-86
lines changed

spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,29 +39,52 @@
3939
import org.springframework.util.StringUtils;
4040

4141
/**
42-
* Implementation of {@link HttpMessageReader} to read 'normal' HTML
43-
* forms with {@code "application/x-www-form-urlencoded"} media type.
42+
* Implementation of an {@link HttpMessageReader} to read HTML form data, i.e.
43+
* request body with media type {@code "application/x-www-form-urlencoded"}.
4444
*
4545
* @author Sebastien Deleuze
46+
* @author Rossen Stoyanchev
47+
* @since 5.0
4648
*/
4749
public class FormHttpMessageReader implements HttpMessageReader<MultiValueMap<String, String>> {
4850

4951
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
5052

51-
private static final ResolvableType formType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
53+
private static final ResolvableType MULTIVALUE_TYPE =
54+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
5255

53-
private Charset charset = DEFAULT_CHARSET;
56+
57+
private Charset defaultCharset = DEFAULT_CHARSET;
58+
59+
60+
/**
61+
* Set the default character set to use for reading form data when the
62+
* request Content-Type header does not explicitly specify it.
63+
* <p>By default this is set to "UTF-8".
64+
*/
65+
public void setDefaultCharset(Charset charset) {
66+
Assert.notNull(charset, "'charset' must not be null");
67+
this.defaultCharset = charset;
68+
}
69+
70+
/**
71+
* Return the configured default charset.
72+
*/
73+
public Charset getDefaultCharset() {
74+
return this.defaultCharset;
75+
}
5476

5577

5678
@Override
5779
public boolean canRead(ResolvableType elementType, MediaType mediaType) {
58-
return (mediaType == null || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) &&
59-
formType.isAssignableFrom(elementType);
80+
return MULTIVALUE_TYPE.isAssignableFrom(elementType) &&
81+
(mediaType == null || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType));
6082
}
6183

6284
@Override
6385
public Flux<MultiValueMap<String, String>> read(ResolvableType elementType,
6486
ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) {
87+
6588
return Flux.from(readMono(elementType, inputMessage, hints));
6689
}
6790

@@ -70,50 +93,52 @@ public Mono<MultiValueMap<String, String>> readMono(ResolvableType elementType,
7093
ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) {
7194

7295
MediaType contentType = inputMessage.getHeaders().getContentType();
73-
Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
96+
Charset charset = getMediaTypeCharset(contentType);
7497

7598
return inputMessage.getBody()
7699
.reduce(DataBuffer::write)
77100
.map(buffer -> {
78101
CharBuffer charBuffer = charset.decode(buffer.asByteBuffer());
79-
DataBufferUtils.release(buffer);
80102
String body = charBuffer.toString();
81-
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
82-
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
83-
try {
84-
for (String pair : pairs) {
85-
int idx = pair.indexOf('=');
86-
if (idx == -1) {
87-
result.add(URLDecoder.decode(pair, charset.name()), null);
88-
}
89-
else {
90-
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
91-
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
92-
result.add(name, value);
93-
}
94-
}
95-
}
96-
catch (UnsupportedEncodingException ex) {
97-
throw new IllegalStateException(ex);
98-
}
99-
100-
return result;
103+
DataBufferUtils.release(buffer);
104+
return parseFormData(charset, body);
101105
});
102106
}
103107

108+
private Charset getMediaTypeCharset(MediaType mediaType) {
109+
if (mediaType != null && mediaType.getCharset() != null) {
110+
return mediaType.getCharset();
111+
}
112+
else {
113+
return getDefaultCharset();
114+
}
115+
}
116+
117+
private MultiValueMap<String, String> parseFormData(Charset charset, String body) {
118+
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
119+
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
120+
try {
121+
for (String pair : pairs) {
122+
int idx = pair.indexOf('=');
123+
if (idx == -1) {
124+
result.add(URLDecoder.decode(pair, charset.name()), null);
125+
}
126+
else {
127+
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
128+
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
129+
result.add(name, value);
130+
}
131+
}
132+
}
133+
catch (UnsupportedEncodingException ex) {
134+
throw new IllegalStateException(ex);
135+
}
136+
return result;
137+
}
138+
104139
@Override
105140
public List<MediaType> getReadableMediaTypes() {
106141
return Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED);
107142
}
108143

109-
/**
110-
* Set the default character set to use for reading form data when the request
111-
* Content-Type header does not explicitly specify it.
112-
* <p>By default this is set to "UTF-8".
113-
*/
114-
public void setCharset(Charset charset) {
115-
Assert.notNull(charset, "'charset' must not be null");
116-
this.charset = charset;
117-
}
118-
119144
}

spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,46 @@
3838
import org.springframework.util.MultiValueMap;
3939

4040
/**
41-
* Implementation of {@link HttpMessageWriter} to write 'normal' HTML
42-
* forms with {@code "application/x-www-form-urlencoded"} media type.
41+
* Implementation of an {@link HttpMessageWriter} to write HTML form data, i.e.
42+
* response body with media type {@code "application/x-www-form-urlencoded"}.
4343
*
4444
* @author Sebastien Deleuze
45+
* @author Rossen Stoyanchev
4546
* @since 5.0
46-
* @see MultiValueMap
4747
*/
4848
public class FormHttpMessageWriter implements HttpMessageWriter<MultiValueMap<String, String>> {
4949

5050
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
5151

52-
private static final ResolvableType formType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
52+
private static final ResolvableType MULTIVALUE_TYPE =
53+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
5354

54-
private Charset charset = DEFAULT_CHARSET;
55+
56+
private Charset defaultCharset = DEFAULT_CHARSET;
57+
58+
59+
/**
60+
* Set the default character set to use for writing form data when the response
61+
* Content-Type header does not explicitly specify it.
62+
* <p>By default this is set to "UTF-8".
63+
*/
64+
public void setDefaultCharset(Charset charset) {
65+
Assert.notNull(charset, "'charset' must not be null");
66+
this.defaultCharset = charset;
67+
}
68+
69+
/**
70+
* Return the configured default charset.
71+
*/
72+
public Charset getDefaultCharset() {
73+
return this.defaultCharset;
74+
}
5575

5676

5777
@Override
5878
public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
59-
return (mediaType == null || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) &&
60-
formType.isAssignableFrom(elementType);
79+
return MULTIVALUE_TYPE.isAssignableFrom(elementType) &&
80+
(mediaType == null || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType));
6181
}
6282

6383
@Override
@@ -66,19 +86,17 @@ public Mono<Void> write(Publisher<? extends MultiValueMap<String, String>> input
6686
Map<String, Object> hints) {
6787

6888
MediaType contentType = outputMessage.getHeaders().getContentType();
69-
Charset charset;
70-
if (contentType != null) {
89+
if (contentType == null) {
90+
contentType = MediaType.APPLICATION_FORM_URLENCODED;
7191
outputMessage.getHeaders().setContentType(contentType);
72-
charset = (contentType != null && contentType.getCharset() != null ? contentType.getCharset() : this.charset);
73-
}
74-
else {
75-
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
76-
charset = this.charset;
7792
}
93+
94+
Charset charset = getMediaTypeCharset(contentType);
95+
7896
return Flux
7997
.from(inputStream)
8098
.single()
81-
.map(form -> generateForm(form))
99+
.map(form -> generateForm(form, charset))
82100
.then(value -> {
83101
ByteBuffer byteBuffer = charset.encode(value);
84102
DataBuffer buffer = outputMessage.bufferFactory().wrap(byteBuffer);
@@ -88,23 +106,32 @@ public Mono<Void> write(Publisher<? extends MultiValueMap<String, String>> input
88106

89107
}
90108

91-
private String generateForm(MultiValueMap<String, String> form) {
109+
private Charset getMediaTypeCharset(MediaType mediaType) {
110+
if (mediaType != null && mediaType.getCharset() != null) {
111+
return mediaType.getCharset();
112+
}
113+
else {
114+
return getDefaultCharset();
115+
}
116+
}
117+
118+
private String generateForm(MultiValueMap<String, String> form, Charset charset) {
92119
StringBuilder builder = new StringBuilder();
93120
try {
94-
for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
95-
String name = nameIterator.next();
96-
for (Iterator<String> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
97-
String value = valueIterator.next();
121+
for (Iterator<String> names = form.keySet().iterator(); names.hasNext();) {
122+
String name = names.next();
123+
for (Iterator<String> values = form.get(name).iterator(); values.hasNext();) {
124+
String value = values.next();
98125
builder.append(URLEncoder.encode(name, charset.name()));
99126
if (value != null) {
100127
builder.append('=');
101128
builder.append(URLEncoder.encode(value, charset.name()));
102-
if (valueIterator.hasNext()) {
129+
if (values.hasNext()) {
103130
builder.append('&');
104131
}
105132
}
106133
}
107-
if (nameIterator.hasNext()) {
134+
if (names.hasNext()) {
108135
builder.append('&');
109136
}
110137
}
@@ -120,14 +147,4 @@ public List<MediaType> getWritableMediaTypes() {
120147
return Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED);
121148
}
122149

123-
/**
124-
* Set the default character set to use for writing form data when the response
125-
* Content-Type header does not explicitly specify it.
126-
* <p>By default this is set to "UTF-8".
127-
*/
128-
public void setCharset(Charset charset) {
129-
Assert.notNull(charset, "'charset' must not be null");
130-
this.charset = charset;
131-
}
132-
133150
}

spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageReaderTests.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,24 @@ public class FormHttpMessageReaderTests {
3636

3737
@Test
3838
public void canRead() {
39-
assertTrue(this.reader.canRead(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
39+
assertTrue(this.reader.canRead(
40+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
4041
MediaType.APPLICATION_FORM_URLENCODED));
41-
assertFalse(this.reader.canRead(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
42+
43+
assertFalse(this.reader.canRead(
44+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
4245
MediaType.APPLICATION_FORM_URLENCODED));
43-
assertFalse(this.reader.canRead(ResolvableType.forClassWithGenerics(MultiValueMap.class, Object.class, String.class),
46+
47+
assertFalse(this.reader.canRead(
48+
ResolvableType.forClassWithGenerics(MultiValueMap.class, Object.class, String.class),
4449
MediaType.APPLICATION_FORM_URLENCODED));
45-
assertFalse(this.reader.canRead(ResolvableType.forClassWithGenerics(Map.class, String.class, String.class),
50+
51+
assertFalse(this.reader.canRead(
52+
ResolvableType.forClassWithGenerics(Map.class, String.class, String.class),
4653
MediaType.APPLICATION_FORM_URLENCODED));
47-
assertFalse(this.reader.canRead(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
54+
55+
assertFalse(this.reader.canRead(
56+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
4857
MediaType.MULTIPART_FORM_DATA));
4958
}
5059

spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,27 @@ public class FormHttpMessageWriterTests {
3737

3838
private final FormHttpMessageWriter writer = new FormHttpMessageWriter();
3939

40+
4041
@Test
4142
public void canWrite() {
42-
assertTrue(this.writer.canWrite(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
43+
assertTrue(this.writer.canWrite(
44+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
4345
MediaType.APPLICATION_FORM_URLENCODED));
44-
assertFalse(this.writer.canWrite(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
46+
47+
assertFalse(this.writer.canWrite(
48+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Object.class),
4549
MediaType.APPLICATION_FORM_URLENCODED));
46-
assertFalse(this.writer.canWrite(ResolvableType.forClassWithGenerics(MultiValueMap.class, Object.class, String.class),
50+
51+
assertFalse(this.writer.canWrite(
52+
ResolvableType.forClassWithGenerics(MultiValueMap.class, Object.class, String.class),
4753
MediaType.APPLICATION_FORM_URLENCODED));
48-
assertFalse(this.writer.canWrite(ResolvableType.forClassWithGenerics(Map.class, String.class, String.class),
54+
55+
assertFalse(this.writer.canWrite(
56+
ResolvableType.forClassWithGenerics(Map.class, String.class, String.class),
4957
MediaType.APPLICATION_FORM_URLENCODED));
50-
assertFalse(this.writer.canWrite(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
58+
59+
assertFalse(this.writer.canWrite(
60+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class),
5161
MediaType.MULTIPART_FORM_DATA));
5262
}
5363

@@ -62,12 +72,9 @@ public void writeForm() {
6272
this.writer.write(Mono.just(body), null, MediaType.APPLICATION_FORM_URLENCODED, response, null).block();
6373

6474
String responseBody = response.getBodyAsString().block();
65-
assertEquals("Invalid result", "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3",
66-
responseBody);
67-
assertEquals("Invalid content-type", MediaType.APPLICATION_FORM_URLENCODED,
68-
response.getHeaders().getContentType());
69-
assertEquals("Invalid content-length", responseBody.getBytes().length,
70-
response.getHeaders().getContentLength());
75+
assertEquals("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3", responseBody);
76+
assertEquals(MediaType.APPLICATION_FORM_URLENCODED, response.getHeaders().getContentType());
77+
assertEquals(responseBody.getBytes().length, response.getHeaders().getContentLength());
7178
}
7279

7380
}

0 commit comments

Comments
 (0)