Browse Source

Added form support to Body[Inserter|Extractor]

- Added BodyInserter for MultiValueMap form data in BodyInserters
 - Added BodyExtractor to MultiValueMap in BodyExtractors

Issue: SPR-15144
pull/1288/merge
Arjen Poutsma 9 years ago
parent
commit
13a7563ddd
  1. 29
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java
  2. 73
      spring-web-reactive/src/main/java/org/springframework/web/reactive/function/BodyInserters.java
  3. 38
      spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java
  4. 37
      spring-web-reactive/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java

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

@ -33,7 +33,9 @@ import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.UnsupportedMediaTypeException; import org.springframework.http.codec.UnsupportedMediaTypeException;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/** /**
* Implementations of {@link BodyExtractor} that read various bodies, such a reactive streams. * Implementations of {@link BodyExtractor} that read various bodies, such a reactive streams.
@ -43,6 +45,10 @@ import org.springframework.util.Assert;
*/ */
public abstract class BodyExtractors { public abstract class BodyExtractors {
private static final ResolvableType FORM_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
/** /**
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}. * Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
* @param elementClass the class of element in the {@code Mono} * @param elementClass the class of element in the {@code Mono}
@ -93,6 +99,29 @@ public abstract class BodyExtractors {
Flux::error); Flux::error);
} }
/**
* Return a {@code BodyExtractor} that reads form data into a {@link MultiValueMap}.
* @return a {@code BodyExtractor} that reads form data
*/
public static BodyExtractor<Mono<MultiValueMap<String, String>>, ServerHttpRequest> toFormData() {
return (serverRequest, context) -> {
HttpMessageReader<MultiValueMap<String, String>> messageReader = formMessageReader(context);
return messageReader.readMono(FORM_TYPE, serverRequest, context.hints());
};
}
private static HttpMessageReader<MultiValueMap<String, String>> formMessageReader(BodyExtractor.Context context) {
return context.messageReaders().get()
.filter(messageReader -> messageReader
.canRead(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
.findFirst()
.map(BodyExtractors::<MultiValueMap<String, String>>cast)
.orElseThrow(() -> new IllegalStateException(
"Could not find HttpMessageReader that supports " +
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
/** /**
* Return a {@code BodyExtractor} that returns the body of the message as a {@link Flux} of * Return a {@code BodyExtractor} that returns the body of the message as a {@link Flux} of
* {@link DataBuffer}s. * {@link DataBuffer}s.

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -29,11 +29,13 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.codec.UnsupportedMediaTypeException; import org.springframework.http.codec.UnsupportedMediaTypeException;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/** /**
* Implementations of {@link BodyInserter} that write various bodies, such a reactive streams, * Implementations of {@link BodyInserter} that write various bodies, such a reactive streams,
@ -49,6 +51,10 @@ public abstract class BodyInserters {
private static final ResolvableType SERVER_SIDE_EVENT_TYPE = private static final ResolvableType SERVER_SIDE_EVENT_TYPE =
ResolvableType.forClass(ServerSentEvent.class); ResolvableType.forClass(ServerSentEvent.class);
private static final ResolvableType FORM_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
private static final BodyInserter<Void, ReactiveHttpOutputMessage> EMPTY = private static final BodyInserter<Void, ReactiveHttpOutputMessage> EMPTY =
(response, context) -> response.setComplete(); (response, context) -> response.setComplete();
@ -109,16 +115,16 @@ public abstract class BodyInserters {
* If the resource can be resolved to a {@linkplain Resource#getFile() file}, it will be copied * If the resource can be resolved to a {@linkplain Resource#getFile() file}, it will be copied
* using * using
* <a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy</a> * <a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy</a>
* @param resource the resource to write to the response * @param resource the resource to write to the output message
* @param <T> the type of the {@code Resource} * @param <T> the type of the {@code Resource}
* @return a {@code BodyInserter} that writes a {@code Publisher} * @return a {@code BodyInserter} that writes a {@code Publisher}
*/ */
public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> fromResource(T resource) { public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> fromResource(T resource) {
Assert.notNull(resource, "'resource' must not be null"); Assert.notNull(resource, "'resource' must not be null");
return (response, context) -> { return (outputMessage, context) -> {
HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context); HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context);
return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, null, return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, null,
response, context.hints()); outputMessage, context.hints());
}; };
} }
@ -143,10 +149,11 @@ public abstract class BodyInserters {
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null"); Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
return (response, context) -> { return (response, context) -> {
HttpMessageWriter<ServerSentEvent<T>> messageWriter = sseMessageWriter(context); HttpMessageWriter<ServerSentEvent<T>> messageWriter =
return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE, findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
MediaType.TEXT_EVENT_STREAM, response, context.hints()); return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
}; MediaType.TEXT_EVENT_STREAM, response, context.hints());
};
} }
/** /**
@ -183,13 +190,45 @@ public abstract class BodyInserters {
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null"); Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
Assert.notNull(eventType, "'eventType' must not be null"); Assert.notNull(eventType, "'eventType' must not be null");
return (outputMessage, context) -> { return (outputMessage, context) -> {
HttpMessageWriter<T> messageWriter = sseMessageWriter(context); HttpMessageWriter<T> messageWriter =
return messageWriter.write(eventsPublisher, eventType, findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints()); return messageWriter.write(eventsPublisher, eventType,
MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints());
}; };
} }
/**
* Return a {@code BodyInserter} that writes the given {@code MultiValueMap} as URL-encoded
* form data.
* @param formData the form data to write to the output message
* @return a {@code BodyInserter} that writes form data
*/
public static BodyInserter<MultiValueMap<String, String>, ClientHttpRequest> fromFormData(MultiValueMap<String, String> formData) {
Assert.notNull(formData, "'formData' must not be null");
return (outputMessage, context) -> {
HttpMessageWriter<MultiValueMap<String, String>> messageWriter =
findMessageWriter(context, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
return messageWriter.write(Mono.just(formData), FORM_TYPE,
MediaType.APPLICATION_FORM_URLENCODED, outputMessage, context.hints());
};
}
private static <T> HttpMessageWriter<T> findMessageWriter(BodyInserter.Context context,
ResolvableType type,
MediaType mediaType) {
return context.messageWriters().get()
.filter(messageWriter -> messageWriter.canWrite(type, mediaType))
.findFirst()
.map(BodyInserters::<T>cast)
.orElseThrow(() -> new IllegalStateException(
"Could not find HttpMessageWriter that supports " + mediaType));
}
/** /**
* Return a {@code BodyInserter} that writes the given {@code Publisher<DataBuffer>} to the * Return a {@code BodyInserter} that writes the given {@code Publisher<DataBuffer>} to the
* body. * body.
@ -204,16 +243,6 @@ public abstract class BodyInserters {
return (outputMessage, context) -> outputMessage.writeWith(publisher); return (outputMessage, context) -> outputMessage.writeWith(publisher);
} }
private static <T> HttpMessageWriter<T> sseMessageWriter(BodyInserter.Context context) {
return context.messageWriters().get()
.filter(messageWriter -> messageWriter
.canWrite(SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM))
.findFirst()
.map(BodyInserters::<T>cast)
.orElseThrow(() -> new IllegalStateException(
"Could not find HttpMessageWriter that supports " +
MediaType.TEXT_EVENT_STREAM_VALUE));
}
private static <T, P extends Publisher<?>, M extends ReactiveHttpOutputMessage> BodyInserter<T, M> bodyInserterFor(P body, ResolvableType bodyType) { private static <T, P extends Publisher<?>, M extends ReactiveHttpOutputMessage> BodyInserter<T, M> bodyInserterFor(P body, ResolvableType bodyType) {

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,11 +41,14 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.FormHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.UnsupportedMediaTypeException; import org.springframework.http.codec.UnsupportedMediaTypeException;
import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.util.MultiValueMap;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@ -68,6 +71,7 @@ public class BodyExtractorsTests {
messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder())); messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder()));
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
messageReaders.add(new FormHttpMessageReader());
this.context = new BodyExtractor.Context() { this.context = new BodyExtractor.Context() {
@Override @Override
@ -79,7 +83,7 @@ public class BodyExtractorsTests {
return hints; return hints;
} }
}; };
this.hints = new HashMap(); this.hints = new HashMap<String, Object>();
} }
@Test @Test
@ -202,6 +206,36 @@ public class BodyExtractorsTests {
.verify(); .verify();
} }
@Test
public void toFormData() throws Exception {
BodyExtractor<Mono<MultiValueMap<String, String>>, ServerHttpRequest> extractor = BodyExtractors.toFormData();
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
DefaultDataBuffer dataBuffer =
factory.wrap(ByteBuffer.wrap("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8)));
Flux<DataBuffer> body = Flux.just(dataBuffer);
MockServerHttpRequest request = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body);
Mono<MultiValueMap<String, String>> result = extractor.extract(request, this.context);
StepVerifier.create(result)
.consumeNextWith(form -> {
assertEquals("Invalid result", 3, form.size());
assertEquals("Invalid result", "value 1", form.getFirst("name 1"));
List<String> values = form.get("name 2");
assertEquals("Invalid result", 2, values.size());
assertEquals("Invalid result", "value 2+1", values.get(0));
assertEquals("Invalid result", "value 2+2", values.get(1));
assertNull("Invalid result", form.getFirst("name 3"));
})
.expectComplete()
.verify();
}
@Test @Test
public void toDataBuffers() throws Exception { public void toDataBuffers() throws Exception {
BodyExtractor<Flux<DataBuffer>, ReactiveHttpInputMessage> extractor = BodyExtractors.toDataBuffers(); BodyExtractor<Flux<DataBuffer>, ReactiveHttpInputMessage> extractor = BodyExtractors.toDataBuffers();

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.web.reactive.function; package org.springframework.web.reactive.function;
import java.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
@ -41,8 +42,11 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.FormHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.codec.ServerSentEvent;
@ -50,7 +54,10 @@ import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.client.reactive.test.MockClientHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
@ -77,6 +84,7 @@ public class BodyInsertersTests {
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
messageWriters messageWriters
.add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder))); .add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
messageWriters.add(new FormHttpMessageWriter());
this.context = new BodyInserter.Context() { this.context = new BodyInserter.Context() {
@Override @Override
@ -198,6 +206,33 @@ public class BodyInsertersTests {
StepVerifier.create(result).expectNextCount(0).expectComplete().verify(); StepVerifier.create(result).expectNextCount(0).expectComplete().verify();
} }
@Test
public void ofFormData() throws Exception {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.set("name 1", "value 1");
body.add("name 2", "value 2+1");
body.add("name 2", "value 2+2");
body.add("name 3", null);
BodyInserter<MultiValueMap<String, String>, ClientHttpRequest>
inserter = BodyInserters.fromFormData(body);
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("http://example.com"));
Mono<Void> result = inserter.insert(request, this.context);
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(request.getBody())
.consumeNextWith(dataBuffer -> {
byte[] resultBytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(resultBytes);
assertArrayEquals("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8),
resultBytes);
})
.expectComplete()
.verify();
}
@Test @Test
public void ofDataBuffers() throws Exception { public void ofDataBuffers() throws Exception {
DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); DefaultDataBufferFactory factory = new DefaultDataBufferFactory();

Loading…
Cancel
Save