Browse Source

Polish + minor refactoring of SSE reader and writer

Instead of accepting List<Encoder|Decoder> and then look for the first
to support JSON, always expect a single JSON [Encoder|Decoder] and use
that unconditionally.

When writing use the nested ResolvableType instead of the Class of the
actual value which should better support generics.

Remove the SSE hint and pass "text/event-stream" as the media type
instead to serve as a hint. We are expecting a JSON encoder and using
it unconditionally in any case so this should be good enough.
pull/1363/head
Rossen Stoyanchev 9 years ago
parent
commit
a999f40daa
  1. 47
      spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java
  2. 159
      spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java
  3. 22
      spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java
  4. 5
      spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageReaderTests.java
  5. 81
      spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageWriterTests.java
  6. 9
      spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java
  7. 10
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java
  8. 6
      spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java
  9. 10
      spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java

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

@ -29,7 +29,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
@ -39,7 +38,6 @@ 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.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
@ -61,18 +59,22 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
private static final StringDecoder stringDecoder = new StringDecoder(false); private static final StringDecoder stringDecoder = new StringDecoder(false);
private final List<Decoder<?>> dataDecoders; private final Decoder<?> decoder;
public ServerSentEventHttpMessageReader() { /**
this.dataDecoders = Collections.emptyList(); * Constructor with JSON {@code Encoder} for encoding objects.
*/
public ServerSentEventHttpMessageReader(Decoder<?> decoder) {
Assert.notNull(decoder, "Decoder must not be null");
this.decoder = decoder;
} }
public ServerSentEventHttpMessageReader(List<Decoder<?>> dataDecoders) {
Assert.notNull(dataDecoders, "'dataDecoders' must not be null");
this.dataDecoders = new ArrayList<>(dataDecoders);
}
@Override
public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.TEXT_EVENT_STREAM);
}
@Override @Override
public boolean canRead(ResolvableType elementType, MediaType mediaType) { public boolean canRead(ResolvableType elementType, MediaType mediaType) {
@ -80,18 +82,13 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
ServerSentEvent.class.isAssignableFrom(elementType.getRawClass()); ServerSentEvent.class.isAssignableFrom(elementType.getRawClass());
} }
@Override
public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.TEXT_EVENT_STREAM);
}
@Override @Override
public Flux<Object> read(ResolvableType elementType, ReactiveHttpInputMessage message, public Flux<Object> read(ResolvableType elementType, ReactiveHttpInputMessage message,
Map<String, Object> hints) { Map<String, Object> hints) {
boolean hasSseWrapper = ServerSentEvent.class.isAssignableFrom(elementType.getRawClass()); boolean shouldWrap = ServerSentEvent.class.isAssignableFrom(elementType.getRawClass());
ResolvableType dataType = (hasSseWrapper ? elementType.getGeneric(0) : elementType); ResolvableType valueType = shouldWrap ? elementType.getGeneric(0) : elementType;
return Flux.from(message.getBody()) return Flux.from(message.getBody())
.concatMap(ServerSentEventHttpMessageReader::splitOnNewline) .concatMap(ServerSentEventHttpMessageReader::splitOnNewline)
@ -103,8 +100,8 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
.bufferUntil(line -> line.equals("\n")) .bufferUntil(line -> line.equals("\n"))
.concatMap(rawLines -> { .concatMap(rawLines -> {
String[] lines = rawLines.stream().collect(joining()).split("\\r?\\n"); String[] lines = rawLines.stream().collect(joining()).split("\\r?\\n");
ServerSentEvent<Object> event = buildEvent(lines, dataType, hints); ServerSentEvent<Object> event = buildEvent(lines, valueType, hints);
return (hasSseWrapper ? Mono.just(event) : Mono.justOrEmpty(event.data())); return (shouldWrap ? Mono.just(event) : Mono.justOrEmpty(event.data()));
}) })
.cast(Object.class); .cast(Object.class);
} }
@ -126,7 +123,8 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
return Flux.fromIterable(results); return Flux.fromIterable(results);
} }
private ServerSentEvent<Object> buildEvent(String[] lines, ResolvableType dataType, Map<String, Object> hints) { private ServerSentEvent<Object> buildEvent(String[] lines, ResolvableType valueType,
Map<String, Object> hints) {
ServerSentEvent.Builder<Object> sseBuilder = ServerSentEvent.builder(); ServerSentEvent.Builder<Object> sseBuilder = ServerSentEvent.builder();
StringBuilder mutableData = new StringBuilder(); StringBuilder mutableData = new StringBuilder();
@ -151,7 +149,7 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
} }
if (mutableData.length() > 0) { if (mutableData.length() > 0) {
String data = mutableData.toString(); String data = mutableData.toString();
sseBuilder.data(decodeData(data, dataType, hints)); sseBuilder.data(decodeData(data, valueType, hints));
} }
if (mutableComment.length() > 0) { if (mutableComment.length() > 0) {
String comment = mutableComment.toString(); String comment = mutableComment.toString();
@ -169,11 +167,8 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
byte[] bytes = data.getBytes(StandardCharsets.UTF_8); byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
Mono<DataBuffer> input = Mono.just(bufferFactory.wrap(bytes)); Mono<DataBuffer> input = Mono.just(bufferFactory.wrap(bytes));
return this.dataDecoders.stream() return this.decoder
.filter(e -> e.canDecode(dataType, MimeTypeUtils.APPLICATION_JSON)) .decodeToMono(input, dataType, MediaType.TEXT_EVENT_STREAM, hints)
.findFirst()
.orElseThrow(() -> new CodecException("No suitable decoder found!"))
.decodeToMono(input, dataType, MimeTypeUtils.APPLICATION_JSON, hints)
.block(Duration.ZERO); .block(Duration.ZERO);
} }
@ -181,7 +176,7 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
public Mono<Object> readMono(ResolvableType elementType, ReactiveHttpInputMessage message, public Mono<Object> readMono(ResolvableType elementType, ReactiveHttpInputMessage message,
Map<String, Object> hints) { Map<String, Object> hints) {
// Let's give StringDecoder a chance since SSE is ordered ahead of it // For single String give StringDecoder a chance which comes after SSE in the order
if (String.class.equals(elementType.getRawClass())) { if (String.class.equals(elementType.getRawClass())) {
Flux<DataBuffer> body = message.getBody(); Flux<DataBuffer> body = message.getBody();

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

@ -17,19 +17,16 @@
package org.springframework.http.codec; package org.springframework.http.codec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.Encoder; import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
@ -38,39 +35,37 @@ import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
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.MimeTypeUtils;
/** /**
* Writer that supports a stream of {@link ServerSentEvent}s and also plain * {@code ServerHttpMessageWriter} for {@code "text/event-stream"} responses.
* {@link Object}s which is the same as an {@link ServerSentEvent} with data
* only.
* *
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
*/ */
public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter<Object> { public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter<Object> {
/** private static final List<MediaType> WRITABLE_MEDIA_TYPES =
* Server-Sent Events hint key expecting a {@link Boolean} value which when set to true Collections.singletonList(MediaType.TEXT_EVENT_STREAM);
* will adapt the content in order to comply with Server-Sent Events recommendation.
* For example, it will append "data:" after each line break with data encoders
* supporting it.
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
*/
public static final String SSE_CONTENT_HINT = ServerSentEventHttpMessageWriter.class.getName() + ".sseContent";
private final List<Encoder<?>> dataEncoders; private final Encoder<?> encoder;
public ServerSentEventHttpMessageWriter() { /**
this.dataEncoders = Collections.emptyList(); * Constructor with JSON {@code Encoder} for encoding objects. Support for
* {@code String} event data is built-in.
*/
public ServerSentEventHttpMessageWriter(Encoder<?> encoder) {
Assert.notNull(encoder, "'encoder' must not be null");
this.encoder = encoder;
} }
public ServerSentEventHttpMessageWriter(List<Encoder<?>> dataEncoders) {
Assert.notNull(dataEncoders, "'dataEncoders' must not be null"); @Override
this.dataEncoders = new ArrayList<>(dataEncoders); public List<MediaType> getWritableMediaTypes() {
return WRITABLE_MEDIA_TYPES;
} }
@ -81,61 +76,35 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter
} }
@Override @Override
public List<MediaType> getWritableMediaTypes() { public Mono<Void> write(Publisher<?> input, ResolvableType elementType, MediaType mediaType,
return Collections.singletonList(MediaType.TEXT_EVENT_STREAM);
}
@Override
public Mono<Void> write(Publisher<?> inputStream, ResolvableType elementType, MediaType mediaType,
ReactiveHttpOutputMessage message, Map<String, Object> hints) { ReactiveHttpOutputMessage message, Map<String, Object> hints) {
message.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM); message.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM);
return message.writeAndFlushWith(encode(input, message.bufferFactory(), elementType, hints));
}
DataBufferFactory bufferFactory = message.bufferFactory(); private Flux<Publisher<DataBuffer>> encode(Publisher<?> input, DataBufferFactory factory,
Flux<Publisher<DataBuffer>> body = encode(inputStream, bufferFactory, elementType, hints); ResolvableType elementType, Map<String, Object> hints) {
return message.writeAndFlushWith(body); ResolvableType valueType = ServerSentEvent.class.isAssignableFrom(elementType.getRawClass()) ?
} elementType.getGeneric(0) : elementType;
private Flux<Publisher<DataBuffer>> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, return Flux.from(input).map(element -> {
ResolvableType type, Map<String, Object> hints) {
Map<String, Object> hintsWithSse = new HashMap<>(hints);
hintsWithSse.put(SSE_CONTENT_HINT, true);
return Flux.from(inputStream)
.map(o -> toSseEvent(o, type))
.map(sse -> {
StringBuilder sb = new StringBuilder();
sse.id().ifPresent(id -> writeField("id", id, sb));
sse.event().ifPresent(event -> writeField("event", event, sb));
sse.retry().ifPresent(retry -> writeField("retry", retry.toMillis(), sb));
sse.comment().ifPresent(comment -> {
comment = comment.replaceAll("\\n", "\n:");
sb.append(':').append(comment).append("\n");
});
Flux<DataBuffer> dataBuffer = sse.data()
.<Flux<DataBuffer>>map(data -> {
sb.append("data:");
if (data instanceof String) {
String stringData = ((String) data).replaceAll("\\n", "\ndata:");
sb.append(stringData).append('\n');
return Flux.empty();
}
else {
return applyEncoder(data, bufferFactory, hintsWithSse);
}
}).orElse(Flux.empty());
return Flux.concat(encodeString(sb.toString(), bufferFactory), dataBuffer,
encodeString("\n", bufferFactory));
});
} ServerSentEvent<?> sse = element instanceof ServerSentEvent ?
(ServerSentEvent<?>) element : ServerSentEvent.builder().data(element).build();
StringBuilder sb = new StringBuilder();
sse.id().ifPresent(v -> writeField("id", v, sb));
sse.event().ifPresent(v -> writeField("event", v, sb));
sse.retry().ifPresent(v -> writeField("retry", v.toMillis(), sb));
sse.comment().ifPresent(v -> sb.append(':').append(v.replaceAll("\\n", "\n:")).append("\n"));
sse.data().ifPresent(v -> sb.append("data:"));
private ServerSentEvent<?> toSseEvent(Object data, ResolvableType type) { return Flux.concat(encodeText(sb, factory),
return ServerSentEvent.class.isAssignableFrom(type.getRawClass()) encodeData(sse, valueType, factory, hints),
? (ServerSentEvent<?>) data encodeText("\n", factory));
: ServerSentEvent.builder().data(data).build(); });
} }
private void writeField(String fieldName, Object fieldValue, StringBuilder stringBuilder) { private void writeField(String fieldName, Object fieldValue, StringBuilder stringBuilder) {
@ -146,40 +115,50 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> Flux<DataBuffer> applyEncoder(Object data, DataBufferFactory bufferFactory, Map<String, Object> hints) { private <T> Flux<DataBuffer> encodeData(ServerSentEvent<?> event, ResolvableType valueType,
ResolvableType elementType = ResolvableType.forClass(data.getClass()); DataBufferFactory factory, Map<String, Object> hints) {
Optional<Encoder<?>> encoder = dataEncoders
.stream() Object data = event.data().orElse(null);
.filter(e -> e.canEncode(elementType, MimeTypeUtils.APPLICATION_JSON)) if (data == null) {
.findFirst(); return Flux.empty();
return ((Encoder<T>) encoder.orElseThrow(() -> new CodecException("No suitable encoder found!"))) }
.encode(Mono.just((T) data), bufferFactory, elementType, MimeTypeUtils.APPLICATION_JSON, hints)
.concatWith(encodeString("\n", bufferFactory)); if (data instanceof String) {
String text = (String) data;
return Flux.from(encodeText(text.replaceAll("\\n", "\ndata:") + "\n", factory));
}
return ((Encoder<T>) this.encoder)
.encode(Mono.just((T) data), factory, valueType, MediaType.TEXT_EVENT_STREAM, hints)
.concatWith(encodeText("\n", factory));
} }
private Mono<DataBuffer> encodeString(String str, DataBufferFactory bufferFactory) { private Mono<DataBuffer> encodeText(CharSequence text, DataBufferFactory bufferFactory) {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8); byte[] bytes = text.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length).write(bytes); DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length).write(bytes);
return Mono.just(buffer); return Mono.just(buffer);
} }
@Override @Override
public Mono<Void> write(Publisher<?> inputStream, ResolvableType actualType, ResolvableType elementType, public Mono<Void> write(Publisher<?> input, ResolvableType actualType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response, MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response,
Map<String, Object> hints) { Map<String, Object> hints) {
Map<String, Object> allHints = this.dataEncoders.stream() Map<String, Object> allHints = new HashMap<>();
.filter(encoder -> encoder instanceof ServerHttpEncoder) allHints.putAll(getEncodeHints(actualType, elementType, mediaType, request, response));
.map(encoder -> (ServerHttpEncoder<?>) encoder)
.map(encoder -> encoder.getEncodeHints(actualType, elementType, mediaType, request, response))
.reduce(new HashMap<>(), (t, u) -> {
t.putAll(u);
return t;
});
allHints.putAll(hints); allHints.putAll(hints);
return write(inputStream, elementType, mediaType, response, allHints); return write(input, elementType, mediaType, response, allHints);
}
private Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
if (this.encoder instanceof ServerHttpEncoder) {
ServerHttpEncoder<?> httpEncoder = (ServerHttpEncoder<?>) this.encoder;
return httpEncoder.getEncodeHints(actualType, elementType, mediaType, request, response);
}
return Collections.emptyMap();
} }
} }

22
spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java

@ -28,7 +28,6 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.type.TypeFactory;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
@ -42,7 +41,6 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerHttpEncoder; import org.springframework.http.codec.ServerHttpEncoder;
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
@ -97,20 +95,24 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerH
Assert.notNull(elementType, "'elementType' must not be null"); Assert.notNull(elementType, "'elementType' must not be null");
if (inputStream instanceof Mono) { if (inputStream instanceof Mono) {
return Flux.from(inputStream).map(value -> encodeValue(value, bufferFactory, elementType, hints)); return Flux.from(inputStream).map(value ->
encodeValue(value, mimeType, bufferFactory, elementType, hints));
} }
else if (APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) { else if (APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) {
return Flux.from(inputStream).map(value -> { return Flux.from(inputStream).map(value -> {
DataBuffer buffer = encodeValue(value, bufferFactory, elementType, hints); DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints);
buffer.write(new byte[]{'\n'}); buffer.write(new byte[]{'\n'});
return buffer; return buffer;
}); });
} }
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); else {
return Flux.from(inputStream).collectList().map(list -> encodeValue(list, bufferFactory, listType, hints)).flux(); ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
return Flux.from(inputStream).collectList().map(list ->
encodeValue(list, mimeType, bufferFactory, listType, hints)).flux();
}
} }
private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactory bufferFactory,
ResolvableType elementType, Map<String, Object> hints) { ResolvableType elementType, Map<String, Object> hints) {
TypeFactory typeFactory = this.mapper.getTypeFactory(); TypeFactory typeFactory = this.mapper.getTypeFactory();
@ -126,9 +128,9 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerH
writer = writer.forType(javaType); writer = writer.forType(javaType);
} }
SerializationConfig config = writer.getConfig(); if (MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mimeType) &&
Boolean sseHint = (Boolean) hints.get(ServerSentEventHttpMessageWriter.SSE_CONTENT_HINT); writer.getConfig().isEnabled(SerializationFeature.INDENT_OUTPUT)) {
if (Boolean.TRUE.equals(sseHint) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
writer = writer.with(this.ssePrettyPrinter); writer = writer.with(this.ssePrettyPrinter);
} }

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

@ -21,7 +21,6 @@ import java.util.Collections;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
@ -39,8 +38,8 @@ import static org.junit.Assert.assertTrue;
*/ */
public class ServerSentEventHttpMessageReaderTests extends AbstractDataBufferAllocatingTestCase { public class ServerSentEventHttpMessageReaderTests extends AbstractDataBufferAllocatingTestCase {
private ServerSentEventHttpMessageReader messageReader = new ServerSentEventHttpMessageReader( private ServerSentEventHttpMessageReader messageReader =
Collections.singletonList(new Jackson2JsonDecoder())); new ServerSentEventHttpMessageReader(new Jackson2JsonDecoder());
@Test @Test
public void cantRead() { public void cantRead() {

81
spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageWriterTests.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.
@ -18,14 +18,15 @@ package org.springframework.http.codec;
import java.time.Duration; import java.time.Duration;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test; import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2JsonEncoder;
@ -34,49 +35,45 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
/** /**
* Unit tests for {@link ServerSentEventHttpMessageWriter}.
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/ */
public class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocatingTestCase { public class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocatingTestCase {
private ServerSentEventHttpMessageWriter messageWriter = new ServerSentEventHttpMessageWriter( public static final Map<String, Object> HINTS = Collections.emptyMap();
Collections.singletonList(new Jackson2JsonEncoder()));
private ServerSentEventHttpMessageWriter messageWriter =
new ServerSentEventHttpMessageWriter(new Jackson2JsonEncoder());
@Test @Test
public void cantRead() { public void canWrite() {
assertFalse(messageWriter.canWrite(ResolvableType.forClass(Object.class), assertTrue(this.messageWriter.canWrite(forClass(Object.class), null));
new MediaType("foo", "bar"))); assertTrue(this.messageWriter.canWrite(null, MediaType.TEXT_EVENT_STREAM));
assertTrue(this.messageWriter.canWrite(forClass(ServerSentEvent.class), new MediaType("foo", "bar")));
} }
@Test @Test
public void canRead() { public void canNotWrite() {
assertTrue(messageWriter.canWrite(ResolvableType.forClass(Object.class), null)); assertFalse(this.messageWriter.canWrite(forClass(Object.class), new MediaType("foo", "bar")));
assertTrue(messageWriter.canWrite(ResolvableType.forClass(Object.class),
new MediaType("text", "event-stream")));
assertTrue(messageWriter.canWrite(ResolvableType.forClass(ServerSentEvent.class),
new MediaType("bar", "bar")));
} }
@Test @Test
public void writeServerSentEvent() { public void writeServerSentEvent() {
ServerSentEvent<String> event = ServerSentEvent.<String>builder().
data("bar").id("c42").event("foo").comment("bla\nbla bla\nbla bla bla")
.retry(Duration.ofMillis(123L)).build();
Mono<ServerSentEvent<String>> source = Mono.just(event); ServerSentEvent<?> event = ServerSentEvent.builder().data("bar").id("c42").event("foo")
.comment("bla\nbla bla\nbla bla bla").retry(Duration.ofMillis(123L)).build();
Mono<ServerSentEvent> source = Mono.just(event);
MockServerHttpResponse outputMessage = new MockServerHttpResponse(); MockServerHttpResponse outputMessage = new MockServerHttpResponse();
messageWriter.write(source, ResolvableType.forClass(ServerSentEvent.class), testWrite(source, outputMessage, ServerSentEvent.class);
new MediaType("text", "event-stream"), outputMessage, Collections.emptyMap()).block(Duration.ofMillis(5000));
StepVerifier.create(outputMessage.getBodyAsString()) StepVerifier.create(outputMessage.getBodyAsString())
.expectNext("id:c42\n" + .expectNext("id:c42\nevent:foo\nretry:123\n:bla\n:bla bla\n:bla bla bla\ndata:bar\n\n")
"event:foo\n" +
"retry:123\n" +
":bla\n" +
":bla bla\n" +
":bla bla bla\n" +
"data:bar\n\n")
.expectComplete() .expectComplete()
.verify(); .verify();
} }
@ -85,8 +82,7 @@ public class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAll
public void writeString() { public void writeString() {
Flux<String> source = Flux.just("foo", "bar"); Flux<String> source = Flux.just("foo", "bar");
MockServerHttpResponse outputMessage = new MockServerHttpResponse(); MockServerHttpResponse outputMessage = new MockServerHttpResponse();
messageWriter.write(source, ResolvableType.forClass(String.class), testWrite(source, outputMessage, String.class);
new MediaType("text", "event-stream"), outputMessage, Collections.emptyMap()).block(Duration.ofMillis(5000));
StepVerifier.create(outputMessage.getBodyAsString()) StepVerifier.create(outputMessage.getBodyAsString())
.expectNext("data:foo\n\ndata:bar\n\n") .expectNext("data:foo\n\ndata:bar\n\n")
@ -98,25 +94,19 @@ public class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAll
public void writeMultiLineString() { public void writeMultiLineString() {
Flux<String> source = Flux.just("foo\nbar", "foo\nbaz"); Flux<String> source = Flux.just("foo\nbar", "foo\nbaz");
MockServerHttpResponse outputMessage = new MockServerHttpResponse(); MockServerHttpResponse outputMessage = new MockServerHttpResponse();
messageWriter.write(source, ResolvableType.forClass(String.class), testWrite(source, outputMessage, String.class);
new MediaType("text", "event-stream"), outputMessage, Collections.emptyMap()).block(Duration.ofMillis(5000));
StepVerifier.create(outputMessage.getBodyAsString()) StepVerifier.create(outputMessage.getBodyAsString())
.expectNext("data:foo\n" + .expectNext("data:foo\ndata:bar\n\ndata:foo\ndata:baz\n\n")
"data:bar\n\n" +
"data:foo\n" +
"data:baz\n\n")
.expectComplete() .expectComplete()
.verify(); .verify();
} }
@Test @Test
public void writePojo() { public void writePojo() {
Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
new Pojo("foofoofoo", "barbarbar"));
MockServerHttpResponse outputMessage = new MockServerHttpResponse(); MockServerHttpResponse outputMessage = new MockServerHttpResponse();
messageWriter.write(source, ResolvableType.forClass(Pojo.class), testWrite(source, outputMessage, Pojo.class);
MediaType.TEXT_EVENT_STREAM, outputMessage, Collections.emptyMap()).block(Duration.ofMillis(5000));
StepVerifier.create(outputMessage.getBodyAsString()) StepVerifier.create(outputMessage.getBodyAsString())
.expectNext("data:{\"foo\":\"foofoo\",\"bar\":\"barbar\"}\n\n" + .expectNext("data:{\"foo\":\"foofoo\",\"bar\":\"barbar\"}\n\n" +
@ -127,15 +117,13 @@ public class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAll
@Test // SPR-14899 @Test // SPR-14899
public void writePojoWithPrettyPrint() { public void writePojoWithPrettyPrint() {
ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().indentOutput(true).build(); ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().indentOutput(true).build();
this.messageWriter = new ServerSentEventHttpMessageWriter( this.messageWriter = new ServerSentEventHttpMessageWriter(new Jackson2JsonEncoder(mapper));
Collections.singletonList(new Jackson2JsonEncoder(mapper)));
Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
new Pojo("foofoofoo", "barbarbar"));
MockServerHttpResponse outputMessage = new MockServerHttpResponse(); MockServerHttpResponse outputMessage = new MockServerHttpResponse();
messageWriter.write(source, ResolvableType.forClass(Pojo.class), testWrite(source, outputMessage, Pojo.class);
MediaType.TEXT_EVENT_STREAM, outputMessage, Collections.emptyMap()).block(Duration.ofMillis(5000));
StepVerifier.create(outputMessage.getBodyAsString()) StepVerifier.create(outputMessage.getBodyAsString())
.expectNext("data:{\n" + .expectNext("data:{\n" +
@ -148,4 +136,9 @@ public class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAll
.verify(); .verify();
} }
private <T> void testWrite(Publisher<T> source, MockServerHttpResponse outputMessage, Class<T> clazz) {
this.messageWriter.write(source, forClass(clazz),
MediaType.TEXT_EVENT_STREAM, outputMessage, HINTS).block(Duration.ofMillis(5000));
}
} }

9
spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java

@ -37,7 +37,6 @@ import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.DataBufferDecoder; import org.springframework.core.codec.DataBufferDecoder;
import org.springframework.core.codec.DataBufferEncoder; import org.springframework.core.codec.DataBufferEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
@ -475,7 +474,6 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
* {@link #configureMessageWriters(List)}. * {@link #configureMessageWriters(List)}.
*/ */
protected final void addDefaultHttpMessageWriters(List<ServerHttpMessageWriter<?>> writers) { protected final void addDefaultHttpMessageWriters(List<ServerHttpMessageWriter<?>> writers) {
List<Encoder<?>> sseDataEncoders = new ArrayList<>();
writers.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); writers.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder())); writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder()));
@ -485,11 +483,10 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
writers.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); writers.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
} }
if (jackson2Present) { if (jackson2Present) {
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
writers.add(new EncoderHttpMessageWriter<>(encoder)); writers.add(new EncoderHttpMessageWriter<>(jacksonEncoder));
sseDataEncoders.add(encoder); writers.add(new ServerSentEventHttpMessageWriter(jacksonEncoder));
} }
writers.add(new ServerSentEventHttpMessageWriter(sseDataEncoders));
} }
/** /**

10
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java

@ -77,7 +77,10 @@ class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder {
private void defaultReaders() { private void defaultReaders() {
messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
messageReader(new ServerSentEventHttpMessageReader(sseDecoders())); if (jackson2Present) {
// SSE ahead of String e.g. "test/event-stream" + Flux<String>
messageReader(new ServerSentEventHttpMessageReader(new Jackson2JsonDecoder()));
}
messageReader(new DecoderHttpMessageReader<>(new StringDecoder(false))); messageReader(new DecoderHttpMessageReader<>(new StringDecoder(false)));
if (jaxb2Present) { if (jaxb2Present) {
messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
@ -87,11 +90,6 @@ class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder {
} }
} }
private List<Decoder<?>> sseDecoders() {
return jackson2Present ? Collections.singletonList(new Jackson2JsonDecoder()) :
Collections.emptyList();
}
private void defaultWriters() { private void defaultWriters() {
messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));

6
spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java

@ -99,11 +99,7 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
messageWriter(new EncoderHttpMessageWriter<>(jsonEncoder)); messageWriter(new EncoderHttpMessageWriter<>(jsonEncoder));
messageWriter( messageWriter(new ServerSentEventHttpMessageWriter(jsonEncoder));
new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
}
else {
messageWriter(new ServerSentEventHttpMessageWriter());
} }
localeResolver(DEFAULT_LOCALE_RESOLVER); localeResolver(DEFAULT_LOCALE_RESOLVER);
} }

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

@ -21,7 +21,6 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -59,9 +58,9 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import static java.nio.charset.StandardCharsets.*; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.*; import static org.junit.Assert.assertArrayEquals;
import static org.springframework.http.codec.json.Jackson2CodecSupport.*; import static org.springframework.http.codec.json.Jackson2CodecSupport.JSON_VIEW_HINT;
/** /**
* @author Arjen Poutsma * @author Arjen Poutsma
@ -83,8 +82,7 @@ public class BodyInsertersTests {
messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
messageWriters messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
.add(new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
messageWriters.add(new FormHttpMessageWriter()); messageWriters.add(new FormHttpMessageWriter());
this.context = new BodyInserter.Context() { this.context = new BodyInserter.Context() {

Loading…
Cancel
Save