Skip to content

Commit 13a7563

Browse files
committed
Added form support to Body[Inserter|Extractor]
- Added BodyInserter for MultiValueMap form data in BodyInserters - Added BodyExtractor to MultiValueMap in BodyExtractors Issue: SPR-15144
1 parent bc87c27 commit 13a7563

File tree

4 files changed

+152
-25
lines changed

4 files changed

+152
-25
lines changed

spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import org.springframework.http.ReactiveHttpInputMessage;
3434
import org.springframework.http.codec.HttpMessageReader;
3535
import org.springframework.http.codec.UnsupportedMediaTypeException;
36+
import org.springframework.http.server.reactive.ServerHttpRequest;
3637
import org.springframework.util.Assert;
38+
import org.springframework.util.MultiValueMap;
3739

3840
/**
3941
* Implementations of {@link BodyExtractor} that read various bodies, such a reactive streams.
@@ -43,6 +45,10 @@
4345
*/
4446
public abstract class BodyExtractors {
4547

48+
private static final ResolvableType FORM_TYPE =
49+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
50+
51+
4652
/**
4753
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
4854
* @param elementClass the class of element in the {@code Mono}
@@ -93,6 +99,29 @@ public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(Resolv
9399
Flux::error);
94100
}
95101

102+
/**
103+
* Return a {@code BodyExtractor} that reads form data into a {@link MultiValueMap}.
104+
* @return a {@code BodyExtractor} that reads form data
105+
*/
106+
public static BodyExtractor<Mono<MultiValueMap<String, String>>, ServerHttpRequest> toFormData() {
107+
return (serverRequest, context) -> {
108+
HttpMessageReader<MultiValueMap<String, String>> messageReader = formMessageReader(context);
109+
return messageReader.readMono(FORM_TYPE, serverRequest, context.hints());
110+
};
111+
}
112+
113+
private static HttpMessageReader<MultiValueMap<String, String>> formMessageReader(BodyExtractor.Context context) {
114+
return context.messageReaders().get()
115+
.filter(messageReader -> messageReader
116+
.canRead(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
117+
.findFirst()
118+
.map(BodyExtractors::<MultiValueMap<String, String>>cast)
119+
.orElseThrow(() -> new IllegalStateException(
120+
"Could not find HttpMessageReader that supports " +
121+
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
122+
}
123+
124+
96125
/**
97126
* Return a {@code BodyExtractor} that returns the body of the message as a {@link Flux} of
98127
* {@link DataBuffer}s.

spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserters.java

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,11 +29,13 @@
2929
import org.springframework.core.io.buffer.DataBuffer;
3030
import org.springframework.http.MediaType;
3131
import org.springframework.http.ReactiveHttpOutputMessage;
32+
import org.springframework.http.client.reactive.ClientHttpRequest;
3233
import org.springframework.http.codec.HttpMessageWriter;
3334
import org.springframework.http.codec.ServerSentEvent;
3435
import org.springframework.http.codec.UnsupportedMediaTypeException;
3536
import org.springframework.http.server.reactive.ServerHttpResponse;
3637
import org.springframework.util.Assert;
38+
import org.springframework.util.MultiValueMap;
3739

3840
/**
3941
* Implementations of {@link BodyInserter} that write various bodies, such a reactive streams,
@@ -49,6 +51,10 @@ public abstract class BodyInserters {
4951
private static final ResolvableType SERVER_SIDE_EVENT_TYPE =
5052
ResolvableType.forClass(ServerSentEvent.class);
5153

54+
private static final ResolvableType FORM_TYPE =
55+
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
56+
57+
5258
private static final BodyInserter<Void, ReactiveHttpOutputMessage> EMPTY =
5359
(response, context) -> response.setComplete();
5460

@@ -109,16 +115,16 @@ public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMess
109115
* If the resource can be resolved to a {@linkplain Resource#getFile() file}, it will be copied
110116
* using
111117
* <a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy</a>
112-
* @param resource the resource to write to the response
118+
* @param resource the resource to write to the output message
113119
* @param <T> the type of the {@code Resource}
114120
* @return a {@code BodyInserter} that writes a {@code Publisher}
115121
*/
116122
public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> fromResource(T resource) {
117123
Assert.notNull(resource, "'resource' must not be null");
118-
return (response, context) -> {
124+
return (outputMessage, context) -> {
119125
HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context);
120126
return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, null,
121-
response, context.hints());
127+
outputMessage, context.hints());
122128
};
123129
}
124130

@@ -143,10 +149,11 @@ public static <T, S extends Publisher<ServerSentEvent<T>>> BodyInserter<S, Serve
143149

144150
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
145151
return (response, context) -> {
146-
HttpMessageWriter<ServerSentEvent<T>> messageWriter = sseMessageWriter(context);
147-
return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
148-
MediaType.TEXT_EVENT_STREAM, response, context.hints());
149-
};
152+
HttpMessageWriter<ServerSentEvent<T>> messageWriter =
153+
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
154+
return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
155+
MediaType.TEXT_EVENT_STREAM, response, context.hints());
156+
};
150157
}
151158

152159
/**
@@ -183,13 +190,45 @@ public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fr
183190
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
184191
Assert.notNull(eventType, "'eventType' must not be null");
185192
return (outputMessage, context) -> {
186-
HttpMessageWriter<T> messageWriter = sseMessageWriter(context);
187-
return messageWriter.write(eventsPublisher, eventType,
188-
MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints());
193+
HttpMessageWriter<T> messageWriter =
194+
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
195+
return messageWriter.write(eventsPublisher, eventType,
196+
MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints());
189197

190-
};
198+
};
191199
}
192200

201+
/**
202+
* Return a {@code BodyInserter} that writes the given {@code MultiValueMap} as URL-encoded
203+
* form data.
204+
* @param formData the form data to write to the output message
205+
* @return a {@code BodyInserter} that writes form data
206+
*/
207+
public static BodyInserter<MultiValueMap<String, String>, ClientHttpRequest> fromFormData(MultiValueMap<String, String> formData) {
208+
Assert.notNull(formData, "'formData' must not be null");
209+
210+
return (outputMessage, context) -> {
211+
HttpMessageWriter<MultiValueMap<String, String>> messageWriter =
212+
findMessageWriter(context, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
213+
return messageWriter.write(Mono.just(formData), FORM_TYPE,
214+
MediaType.APPLICATION_FORM_URLENCODED, outputMessage, context.hints());
215+
};
216+
}
217+
218+
private static <T> HttpMessageWriter<T> findMessageWriter(BodyInserter.Context context,
219+
ResolvableType type,
220+
MediaType mediaType) {
221+
222+
return context.messageWriters().get()
223+
.filter(messageWriter -> messageWriter.canWrite(type, mediaType))
224+
.findFirst()
225+
.map(BodyInserters::<T>cast)
226+
.orElseThrow(() -> new IllegalStateException(
227+
"Could not find HttpMessageWriter that supports " + mediaType));
228+
}
229+
230+
231+
193232
/**
194233
* Return a {@code BodyInserter} that writes the given {@code Publisher<DataBuffer>} to the
195234
* body.
@@ -204,16 +243,6 @@ public static <T extends Publisher<DataBuffer>> BodyInserter<T, ReactiveHttpOutp
204243
return (outputMessage, context) -> outputMessage.writeWith(publisher);
205244
}
206245

207-
private static <T> HttpMessageWriter<T> sseMessageWriter(BodyInserter.Context context) {
208-
return context.messageWriters().get()
209-
.filter(messageWriter -> messageWriter
210-
.canWrite(SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM))
211-
.findFirst()
212-
.map(BodyInserters::<T>cast)
213-
.orElseThrow(() -> new IllegalStateException(
214-
"Could not find HttpMessageWriter that supports " +
215-
MediaType.TEXT_EVENT_STREAM_VALUE));
216-
}
217246

218247
private static <T, P extends Publisher<?>, M extends ReactiveHttpOutputMessage> BodyInserter<T, M> bodyInserterFor(P body, ResolvableType bodyType) {
219248

spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,11 +41,14 @@
4141
import org.springframework.http.MediaType;
4242
import org.springframework.http.ReactiveHttpInputMessage;
4343
import org.springframework.http.codec.DecoderHttpMessageReader;
44+
import org.springframework.http.codec.FormHttpMessageReader;
4445
import org.springframework.http.codec.HttpMessageReader;
4546
import org.springframework.http.codec.UnsupportedMediaTypeException;
4647
import org.springframework.http.codec.json.Jackson2JsonDecoder;
4748
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
49+
import org.springframework.http.server.reactive.ServerHttpRequest;
4850
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
51+
import org.springframework.util.MultiValueMap;
4952

5053
import static org.junit.Assert.assertEquals;
5154
import static org.junit.Assert.assertNull;
@@ -68,6 +71,7 @@ public void createContext() {
6871
messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder()));
6972
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
7073
messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
74+
messageReaders.add(new FormHttpMessageReader());
7175

7276
this.context = new BodyExtractor.Context() {
7377
@Override
@@ -79,7 +83,7 @@ public Map<String, Object> hints() {
7983
return hints;
8084
}
8185
};
82-
this.hints = new HashMap();
86+
this.hints = new HashMap<String, Object>();
8387
}
8488

8589
@Test
@@ -202,6 +206,36 @@ public Map<String, Object> hints() {
202206
.verify();
203207
}
204208

209+
@Test
210+
public void toFormData() throws Exception {
211+
BodyExtractor<Mono<MultiValueMap<String, String>>, ServerHttpRequest> extractor = BodyExtractors.toFormData();
212+
213+
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
214+
DefaultDataBuffer dataBuffer =
215+
factory.wrap(ByteBuffer.wrap("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8)));
216+
Flux<DataBuffer> body = Flux.just(dataBuffer);
217+
218+
MockServerHttpRequest request = MockServerHttpRequest.post("/")
219+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
220+
.body(body);
221+
222+
Mono<MultiValueMap<String, String>> result = extractor.extract(request, this.context);
223+
224+
StepVerifier.create(result)
225+
.consumeNextWith(form -> {
226+
assertEquals("Invalid result", 3, form.size());
227+
assertEquals("Invalid result", "value 1", form.getFirst("name 1"));
228+
List<String> values = form.get("name 2");
229+
assertEquals("Invalid result", 2, values.size());
230+
assertEquals("Invalid result", "value 2+1", values.get(0));
231+
assertEquals("Invalid result", "value 2+2", values.get(1));
232+
assertNull("Invalid result", form.getFirst("name 3"));
233+
})
234+
.expectComplete()
235+
.verify();
236+
237+
}
238+
205239
@Test
206240
public void toDataBuffers() throws Exception {
207241
BodyExtractor<Flux<DataBuffer>, ReactiveHttpInputMessage> extractor = BodyExtractors.toDataBuffers();

spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.reactive.function;
1818

19+
import java.net.URI;
1920
import java.nio.ByteBuffer;
2021
import java.nio.charset.StandardCharsets;
2122
import java.nio.file.Files;
@@ -41,16 +42,22 @@
4142
import org.springframework.core.io.buffer.DataBuffer;
4243
import org.springframework.core.io.buffer.DefaultDataBuffer;
4344
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
45+
import org.springframework.http.HttpMethod;
4446
import org.springframework.http.ReactiveHttpOutputMessage;
47+
import org.springframework.http.client.reactive.ClientHttpRequest;
4548
import org.springframework.http.codec.EncoderHttpMessageWriter;
49+
import org.springframework.http.codec.FormHttpMessageWriter;
4650
import org.springframework.http.codec.HttpMessageWriter;
4751
import org.springframework.http.codec.ResourceHttpMessageWriter;
4852
import org.springframework.http.codec.ServerSentEvent;
4953
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
5054
import org.springframework.http.codec.json.Jackson2JsonEncoder;
5155
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
5256
import org.springframework.http.server.reactive.ServerHttpResponse;
57+
import org.springframework.mock.http.client.reactive.test.MockClientHttpRequest;
5358
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
59+
import org.springframework.util.LinkedMultiValueMap;
60+
import org.springframework.util.MultiValueMap;
5461

5562
import static java.nio.charset.StandardCharsets.UTF_8;
5663
import static org.junit.Assert.assertArrayEquals;
@@ -77,6 +84,7 @@ public void createContext() {
7784
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
7885
messageWriters
7986
.add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
87+
messageWriters.add(new FormHttpMessageWriter());
8088

8189
this.context = new BodyInserter.Context() {
8290
@Override
@@ -198,6 +206,33 @@ public void ofServerSentEventClass() throws Exception {
198206
StepVerifier.create(result).expectNextCount(0).expectComplete().verify();
199207
}
200208

209+
@Test
210+
public void ofFormData() throws Exception {
211+
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
212+
body.set("name 1", "value 1");
213+
body.add("name 2", "value 2+1");
214+
body.add("name 2", "value 2+2");
215+
body.add("name 3", null);
216+
217+
BodyInserter<MultiValueMap<String, String>, ClientHttpRequest>
218+
inserter = BodyInserters.fromFormData(body);
219+
220+
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("http://example.com"));
221+
Mono<Void> result = inserter.insert(request, this.context);
222+
StepVerifier.create(result).expectComplete().verify();
223+
224+
StepVerifier.create(request.getBody())
225+
.consumeNextWith(dataBuffer -> {
226+
byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
227+
dataBuffer.read(resultBytes);
228+
assertArrayEquals("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8),
229+
resultBytes);
230+
})
231+
.expectComplete()
232+
.verify();
233+
234+
}
235+
201236
@Test
202237
public void ofDataBuffers() throws Exception {
203238
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();

0 commit comments

Comments
 (0)