Added form support to Body[Inserter|Extractor]
- Added BodyInserter for MultiValueMap form data in BodyInserters - Added BodyExtractor to MultiValueMap in BodyExtractors Issue: SPR-15144
This commit is contained in:
@@ -33,7 +33,9 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.UnsupportedMediaTypeException;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
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}.
|
||||
* @param elementClass the class of element in the {@code Mono}
|
||||
@@ -93,6 +99,29 @@ public abstract class BodyExtractors {
|
||||
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
|
||||
* {@link DataBuffer}s.
|
||||
|
||||
@@ -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");
|
||||
* 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.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.http.codec.UnsupportedMediaTypeException;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
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 =
|
||||
(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
|
||||
* using
|
||||
* <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}
|
||||
* @return a {@code BodyInserter} that writes a {@code Publisher}
|
||||
*/
|
||||
public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> fromResource(T resource) {
|
||||
Assert.notNull(resource, "'resource' must not be null");
|
||||
return (response, context) -> {
|
||||
return (outputMessage, context) -> {
|
||||
HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context);
|
||||
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");
|
||||
return (response, context) -> {
|
||||
HttpMessageWriter<ServerSentEvent<T>> messageWriter = sseMessageWriter(context);
|
||||
return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
|
||||
MediaType.TEXT_EVENT_STREAM, response, context.hints());
|
||||
};
|
||||
HttpMessageWriter<ServerSentEvent<T>> messageWriter =
|
||||
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
|
||||
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(eventType, "'eventType' must not be null");
|
||||
return (outputMessage, context) -> {
|
||||
HttpMessageWriter<T> messageWriter = sseMessageWriter(context);
|
||||
return messageWriter.write(eventsPublisher, eventType,
|
||||
MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints());
|
||||
HttpMessageWriter<T> messageWriter =
|
||||
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
|
||||
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
|
||||
* body.
|
||||
@@ -204,16 +243,6 @@ public abstract class BodyInserters {
|
||||
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) {
|
||||
|
||||
|
||||
@@ -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");
|
||||
* 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.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.FormHttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.UnsupportedMediaTypeException;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
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.util.MultiValueMap;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
@@ -68,6 +71,7 @@ public class BodyExtractorsTests {
|
||||
messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder()));
|
||||
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
||||
messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
||||
messageReaders.add(new FormHttpMessageReader());
|
||||
|
||||
this.context = new BodyExtractor.Context() {
|
||||
@Override
|
||||
@@ -79,7 +83,7 @@ public class BodyExtractorsTests {
|
||||
return hints;
|
||||
}
|
||||
};
|
||||
this.hints = new HashMap();
|
||||
this.hints = new HashMap<String, Object>();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -202,6 +206,36 @@ public class BodyExtractorsTests {
|
||||
.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
|
||||
public void toDataBuffers() throws Exception {
|
||||
BodyExtractor<Flux<DataBuffer>, ReactiveHttpInputMessage> extractor = BodyExtractors.toDataBuffers();
|
||||
|
||||
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.web.reactive.function;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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.DefaultDataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.FormHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
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.xml.Jaxb2XmlEncoder;
|
||||
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.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
@@ -77,6 +84,7 @@ public class BodyInsertersTests {
|
||||
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
|
||||
messageWriters
|
||||
.add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
|
||||
messageWriters.add(new FormHttpMessageWriter());
|
||||
|
||||
this.context = new BodyInserter.Context() {
|
||||
@Override
|
||||
@@ -198,6 +206,33 @@ public class BodyInsertersTests {
|
||||
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
|
||||
public void ofDataBuffers() throws Exception {
|
||||
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
|
||||
|
||||
Reference in New Issue
Block a user