Skip to content

Commit 3d68c49

Browse files
committed
String encoding for any MIME type
CharSequenceEncoder now supports all MIME types, however since encoding Flux<String> can overlap with other encoders (e.g. SSE) there are now two ways to create a CharSequenceEncoder -- with support for text/plain only or with support for any MIME type. In WebFlux configuration we insert one CharSequenceEncoder for text/plain (as we have so far) and a second instance with support for any MIME type at the very end. Issue: SPR-15374
1 parent 2896c5d commit 3d68c49

File tree

16 files changed

+106
-39
lines changed

16 files changed

+106
-39
lines changed

spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java

Lines changed: 20 additions & 3 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,12 +29,14 @@
2929
import org.springframework.core.io.buffer.DataBuffer;
3030
import org.springframework.core.io.buffer.DataBufferFactory;
3131
import org.springframework.util.MimeType;
32+
import org.springframework.util.MimeTypeUtils;
3233

3334
/**
3435
* Encode from a {@code CharSequence} stream to a bytes stream.
3536
*
3637
* @author Sebastien Deleuze
3738
* @author Arjen Poutsma
39+
* @author Rossen Stoyanchev
3840
* @since 5.0
3941
* @see StringDecoder
4042
*/
@@ -43,8 +45,8 @@ public class CharSequenceEncoder extends AbstractEncoder<CharSequence> {
4345
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
4446

4547

46-
public CharSequenceEncoder() {
47-
super(new MimeType("text", "plain", DEFAULT_CHARSET));
48+
private CharSequenceEncoder(MimeType... mimeTypes) {
49+
super(mimeTypes);
4850
}
4951

5052

@@ -73,4 +75,19 @@ public Flux<DataBuffer> encode(Publisher<? extends CharSequence> inputStream,
7375
});
7476
}
7577

78+
79+
/**
80+
* Create a {@code CharSequenceEncoder} that supports only "text/plain".
81+
*/
82+
public static CharSequenceEncoder textPlainOnly() {
83+
return new CharSequenceEncoder(new MimeType("text", "plain", DEFAULT_CHARSET));
84+
}
85+
86+
/**
87+
* Create a {@code CharSequenceEncoder} that supports all MIME types.
88+
*/
89+
public static CharSequenceEncoder allMimeTypes() {
90+
return new CharSequenceEncoder(new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
91+
}
92+
7693
}

spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class CharSequenceEncoderTests extends AbstractDataBufferAllocatingTestCa
4343

4444
@Before
4545
public void createEncoder() {
46-
this.encoder = new CharSequenceEncoder();
46+
this.encoder = CharSequenceEncoder.textPlainOnly();
4747
}
4848

4949
@Test

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
3838
import org.springframework.http.MediaType;
3939
import org.springframework.http.ReactiveHttpInputMessage;
40-
import org.springframework.util.Assert;
4140

4241
import static java.util.stream.Collectors.joining;
4342

@@ -63,10 +62,18 @@ public class ServerSentEventHttpMessageReader implements HttpMessageReader<Objec
6362

6463

6564
/**
66-
* Constructor with JSON {@code Encoder} for encoding objects.
65+
* Constructor without a {@code Decoder}. In this mode only {@code String}
66+
* is supported as the data of an event.
67+
*/
68+
public ServerSentEventHttpMessageReader() {
69+
this(null);
70+
}
71+
72+
/**
73+
* Constructor with JSON {@code Decoder} for decoding to Objects. Support
74+
* for decoding to {@code String} event data is built-in.
6775
*/
6876
public ServerSentEventHttpMessageReader(Decoder<?> decoder) {
69-
Assert.notNull(decoder, "Decoder must not be null");
7077
this.decoder = decoder;
7178
}
7279

@@ -85,7 +92,7 @@ public List<MediaType> getReadableMediaTypes() {
8592

8693
@Override
8794
public boolean canRead(ResolvableType elementType, MediaType mediaType) {
88-
return MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mediaType) ||
95+
return MediaType.TEXT_EVENT_STREAM.includes(mediaType) ||
8996
ServerSentEvent.class.isAssignableFrom(elementType.getRawClass());
9097
}
9198

@@ -183,8 +190,6 @@ private Object decodeData(String data, ResolvableType dataType, Map<String, Obje
183190
public Mono<Object> readMono(ResolvableType elementType, ReactiveHttpInputMessage message,
184191
Map<String, Object> hints) {
185192

186-
// For single String give StringDecoder a chance which comes after SSE in the order
187-
188193
if (String.class.equals(elementType.getRawClass())) {
189194
Flux<DataBuffer> body = message.getBody();
190195
return stringDecoder.decodeToMono(body, elementType, null, null).cast(Object.class);

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@
2727
import reactor.core.publisher.Mono;
2828

2929
import org.springframework.core.ResolvableType;
30+
import org.springframework.core.codec.CodecException;
3031
import org.springframework.core.codec.Encoder;
3132
import org.springframework.core.io.buffer.DataBuffer;
3233
import org.springframework.core.io.buffer.DataBufferFactory;
3334
import org.springframework.http.MediaType;
3435
import org.springframework.http.ReactiveHttpOutputMessage;
3536
import org.springframework.http.server.reactive.ServerHttpRequest;
3637
import org.springframework.http.server.reactive.ServerHttpResponse;
37-
import org.springframework.util.Assert;
3838

3939
/**
4040
* {@code ServerHttpMessageWriter} for {@code "text/event-stream"} responses.
@@ -53,18 +53,25 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter
5353
private final Encoder<?> encoder;
5454

5555

56+
/**
57+
* Constructor without an {@code Encoder}. In this mode only {@code String}
58+
* is supported for event data to be encoded.
59+
*/
60+
public ServerSentEventHttpMessageWriter() {
61+
this(null);
62+
}
63+
5664
/**
5765
* Constructor with JSON {@code Encoder} for encoding objects. Support for
5866
* {@code String} event data is built-in.
5967
*/
6068
public ServerSentEventHttpMessageWriter(Encoder<?> encoder) {
61-
Assert.notNull(encoder, "'encoder' must not be null");
6269
this.encoder = encoder;
6370
}
6471

6572

6673
/**
67-
* Return the configured {@code Encoder}.
74+
* Return the configured {@code Encoder}, possibly {@code null}.
6875
*/
6976
public Encoder<?> getEncoder() {
7077
return this.encoder;
@@ -78,7 +85,7 @@ public List<MediaType> getWritableMediaTypes() {
7885

7986
@Override
8087
public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
81-
return mediaType == null || MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mediaType) ||
88+
return mediaType == null || MediaType.TEXT_EVENT_STREAM.includes(mediaType) ||
8289
ServerSentEvent.class.isAssignableFrom(elementType.getRawClass());
8390
}
8491

@@ -135,6 +142,10 @@ private <T> Flux<DataBuffer> encodeData(ServerSentEvent<?> event, ResolvableType
135142
return Flux.from(encodeText(text.replaceAll("\\n", "\ndata:") + "\n", factory));
136143
}
137144

145+
if (this.encoder == null) {
146+
return Flux.error(new CodecException("No SSE encoder configured and the data is not String."));
147+
}
148+
138149
return ((Encoder<T>) this.encoder)
139150
.encode(Mono.just((T) data), factory, valueType, MediaType.TEXT_EVENT_STREAM, hints)
140151
.concatWith(encodeText("\n", factory));

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.core.codec.CharSequenceEncoder;
3838
import org.springframework.core.codec.DataBufferDecoder;
3939
import org.springframework.core.codec.DataBufferEncoder;
40+
import org.springframework.core.codec.Encoder;
4041
import org.springframework.core.codec.ResourceDecoder;
4142
import org.springframework.core.codec.StringDecoder;
4243
import org.springframework.core.convert.converter.Converter;
@@ -469,6 +470,7 @@ protected final List<ServerHttpMessageWriter<?>> getMessageWriters() {
469470
*/
470471
protected void configureMessageWriters(List<ServerHttpMessageWriter<?>> messageWriters) {
471472
}
473+
472474
/**
473475
* Adds default converters that sub-classes can call from
474476
* {@link #configureMessageWriters(List)}.
@@ -477,15 +479,24 @@ protected final void addDefaultHttpMessageWriters(List<ServerHttpMessageWriter<?
477479
writers.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
478480
writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
479481
writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder()));
480-
writers.add(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
482+
writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
481483
writers.add(new ResourceHttpMessageWriter());
482484
if (jaxb2Present) {
483485
writers.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
484486
}
485487
if (jackson2Present) {
486-
Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
487-
writers.add(new EncoderHttpMessageWriter<>(jacksonEncoder));
488-
writers.add(new ServerSentEventHttpMessageWriter(jacksonEncoder));
488+
writers.add(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()));
489+
}
490+
writers.add(new ServerSentEventHttpMessageWriter(getSseEncoder()));
491+
writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
492+
}
493+
494+
private Encoder<?> getSseEncoder() {
495+
if (jackson2Present) {
496+
return new Jackson2JsonEncoder();
497+
}
498+
else {
499+
return null;
489500
}
490501
}
491502

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,10 @@ public void defaultConfiguration() {
7575
}
7676

7777
private void defaultReaders() {
78+
// SSE first (constrained to "text/event-stream")
79+
messageReader(new ServerSentEventHttpMessageReader(getSseDecoder()));
7880
messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
7981
messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
80-
if (jackson2Present) {
81-
// SSE ahead of String e.g. "test/event-stream" + Flux<String>
82-
messageReader(new ServerSentEventHttpMessageReader(new Jackson2JsonDecoder()));
83-
}
8482
messageReader(new DecoderHttpMessageReader<>(new StringDecoder(false)));
8583
if (jaxb2Present) {
8684
messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
@@ -90,10 +88,19 @@ private void defaultReaders() {
9088
}
9189
}
9290

91+
private Decoder<?> getSseDecoder() {
92+
if (jackson2Present) {
93+
return new Jackson2JsonDecoder();
94+
}
95+
else {
96+
return null;
97+
}
98+
}
99+
93100
private void defaultWriters() {
94101
messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
95102
messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
96-
messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
103+
messageWriter(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
97104
messageWriter(new ResourceHttpMessageWriter());
98105
messageWriter(new FormHttpMessageWriter());
99106
if (jaxb2Present) {

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.core.codec.ByteBufferDecoder;
3232
import org.springframework.core.codec.ByteBufferEncoder;
3333
import org.springframework.core.codec.CharSequenceEncoder;
34+
import org.springframework.core.codec.Encoder;
3435
import org.springframework.core.codec.StringDecoder;
3536
import org.springframework.http.codec.DecoderHttpMessageReader;
3637
import org.springframework.http.codec.EncoderHttpMessageWriter;
@@ -88,7 +89,7 @@ public void defaultConfiguration() {
8889

8990
messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
9091
messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
91-
messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
92+
messageWriter(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
9293
messageWriter(new ResourceHttpMessageWriter());
9394

9495
if (jaxb2Present) {
@@ -97,13 +98,24 @@ public void defaultConfiguration() {
9798
}
9899
if (jackson2Present) {
99100
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
100-
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
101-
messageWriter(new EncoderHttpMessageWriter<>(jsonEncoder));
102-
messageWriter(new ServerSentEventHttpMessageWriter(jsonEncoder));
101+
messageWriter(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()));
103102
}
103+
104+
messageWriter(new ServerSentEventHttpMessageWriter(getSseEncoder()));
105+
messageWriter(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
106+
104107
localeResolver(DEFAULT_LOCALE_RESOLVER);
105108
}
106109

110+
private Encoder<?> getSseEncoder() {
111+
if (jackson2Present) {
112+
return new Jackson2JsonEncoder();
113+
}
114+
else {
115+
return null;
116+
}
117+
}
118+
107119
public void applicationContext(ApplicationContext applicationContext) {
108120
applicationContext.getBeansOfType(HttpMessageReader.class).values().forEach(this::messageReader);
109121
applicationContext.getBeansOfType(HttpMessageWriter.class).values().forEach(this::messageWriter);

spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ public RequestMappingHandlerAdapter handlerAdapter() {
186186

187187
@Bean
188188
public ResponseBodyResultHandler resultHandler() {
189-
return new ResponseBodyResultHandler(
190-
Collections.singletonList(new EncoderHttpMessageWriter<>(new CharSequenceEncoder())),
189+
return new ResponseBodyResultHandler(Collections.singletonList(
190+
new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())),
191191
new HeaderContentTypeResolver());
192192
}
193193

spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public void responseEntityResultHandler() throws Exception {
178178
assertEquals(0, handler.getOrder());
179179

180180
List<ServerHttpMessageWriter<?>> writers = handler.getMessageWriters();
181-
assertEquals(8, writers.size());
181+
assertEquals(9, writers.size());
182182

183183
assertHasMessageWriter(writers, byte[].class, APPLICATION_OCTET_STREAM);
184184
assertHasMessageWriter(writers, ByteBuffer.class, APPLICATION_OCTET_STREAM);
@@ -204,7 +204,7 @@ public void responseBodyResultHandler() throws Exception {
204204
assertEquals(100, handler.getOrder());
205205

206206
List<ServerHttpMessageWriter<?>> writers = handler.getMessageWriters();
207-
assertEquals(8, writers.size());
207+
assertEquals(9, writers.size());
208208

209209
assertHasMessageWriter(writers, byte[].class, APPLICATION_OCTET_STREAM);
210210
assertHasMessageWriter(writers, ByteBuffer.class, APPLICATION_OCTET_STREAM);
@@ -303,7 +303,7 @@ protected void configureMessageReaders(List<ServerHttpMessageReader<?>> messageR
303303

304304
@Override
305305
protected void configureMessageWriters(List<ServerHttpMessageWriter<?>> messageWriters) {
306-
messageWriters.add(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
306+
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
307307
}
308308

309309
@Override

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,14 @@ public class BodyInsertersTests {
7777
public void createContext() {
7878
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
7979
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
80-
messageWriters.add(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
80+
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
8181
messageWriters.add(new ResourceHttpMessageWriter());
8282
messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
8383
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
8484
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
8585
messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
8686
messageWriters.add(new FormHttpMessageWriter());
87+
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
8788

8889
this.context = new BodyInserter.Context() {
8990
@Override

0 commit comments

Comments
 (0)