Skip to content

Commit 32f6cce

Browse files
committed
Add WebFlux support for Smile streaming
The commit brings following changes: - Move getDecodableMimeTypes() to AbstractJackson2Decoder - Move getEncodableMimeTypes() to AbstractJackson2Encoder - Add support for application/stream+x-jackson-smile - Avoid streaming line separator when Smile encoder is used - Use double null token in Jackson2Tokenizer to identify documents Issue: SPR-16151
1 parent e1fa65a commit 32f6cce

File tree

12 files changed

+112
-50
lines changed

12 files changed

+112
-50
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.lang.annotation.Annotation;
21+
import java.util.List;
2122
import java.util.Map;
2223

2324
import com.fasterxml.jackson.core.JsonFactory;
@@ -133,6 +134,13 @@ public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableT
133134
return getHints(actualType);
134135
}
135136

137+
@Override
138+
public List<MimeType> getDecodableMimeTypes() {
139+
return getMimeTypes();
140+
}
141+
142+
// Jackson2CodecSupport ...
143+
136144
@Override
137145
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
138146
return parameter.getParameterAnnotation(annotType);

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -60,6 +60,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
6060

6161
protected final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
6262

63+
protected boolean streamingLineSeparator = true;
64+
6365

6466
/**
6567
* Constructor with a Jackson {@link ObjectMapper} to use.
@@ -104,7 +106,9 @@ public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory buffe
104106
else if (this.streamingMediaTypes.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(mimeType))) {
105107
return Flux.from(inputStream).map(value -> {
106108
DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints);
107-
buffer.write(new byte[]{'\n'});
109+
if (streamingLineSeparator) {
110+
buffer.write(new byte[]{'\n'});
111+
}
108112
return buffer;
109113
});
110114
}
@@ -156,6 +160,11 @@ protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType m
156160

157161
// HttpMessageEncoder...
158162

163+
@Override
164+
public List<MimeType> getEncodableMimeTypes() {
165+
return getMimeTypes();
166+
}
167+
159168
@Override
160169
public List<MediaType> getStreamingMediaTypes() {
161170
return Collections.unmodifiableList(this.streamingMediaTypes);
@@ -168,6 +177,8 @@ public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, R
168177
return (actualType != null ? getHints(actualType) : Collections.emptyMap());
169178
}
170179

180+
// Jackson2CodecSupport ...
181+
171182
@Override
172183
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
173184
return parameter.getMethodAnnotation(annotType);

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

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

1717
package org.springframework.http.codec.json;
1818

19-
import java.util.List;
20-
2119
import com.fasterxml.jackson.databind.ObjectMapper;
2220

2321
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@@ -42,10 +40,4 @@ public Jackson2JsonDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
4240
super(mapper, mimeTypes);
4341
}
4442

45-
46-
@Override
47-
public List<MimeType> getDecodableMimeTypes() {
48-
return getMimeTypes();
49-
}
50-
5143
}

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -44,7 +44,7 @@
4444
* @see Jackson2JsonDecoder
4545
*/
4646
public class Jackson2JsonEncoder extends AbstractJackson2Encoder {
47-
47+
4848
@Nullable
4949
private final PrettyPrinter ssePrettyPrinter;
5050

@@ -76,9 +76,4 @@ protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType m
7676
writer.with(this.ssePrettyPrinter) : writer);
7777
}
7878

79-
@Override
80-
public List<MimeType> getEncodableMimeTypes() {
81-
return getMimeTypes();
82-
}
83-
8479
}

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

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

1717
package org.springframework.http.codec.json;
1818

19+
import java.nio.charset.StandardCharsets;
20+
import java.util.Arrays;
1921
import java.util.Collections;
2022
import java.util.List;
2123

@@ -38,21 +40,18 @@
3840
*/
3941
public class Jackson2SmileDecoder extends AbstractJackson2Decoder {
4042

41-
private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile");
43+
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
44+
new MimeType("application", "x-jackson-smile", StandardCharsets.UTF_8),
45+
new MimeType("application", "*+x-jackson-smile", StandardCharsets.UTF_8)};
4246

4347

4448
public Jackson2SmileDecoder() {
45-
this(Jackson2ObjectMapperBuilder.smile().build(), SMILE_MIME_TYPE);
49+
this(Jackson2ObjectMapperBuilder.smile().build(), DEFAULT_SMILE_MIME_TYPES);
4650
}
4751

4852
public Jackson2SmileDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
4953
super(mapper, mimeTypes);
5054
Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass());
5155
}
5256

53-
@Override
54-
public List<MimeType> getDecodableMimeTypes() {
55-
return Collections.singletonList(SMILE_MIME_TYPE);
56-
}
57-
5857
}

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

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

1717
package org.springframework.http.codec.json;
1818

19+
import java.nio.charset.StandardCharsets;
1920
import java.util.Collections;
2021
import java.util.List;
2122

@@ -39,23 +40,20 @@
3940
*/
4041
public class Jackson2SmileEncoder extends AbstractJackson2Encoder {
4142

42-
private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile");
43+
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
44+
new MimeType("application", "x-jackson-smile", StandardCharsets.UTF_8),
45+
new MimeType("application", "*+x-jackson-smile", StandardCharsets.UTF_8)};
4346

4447

4548
public Jackson2SmileEncoder() {
46-
this(Jackson2ObjectMapperBuilder.smile().build(), new MediaType("application", "x-jackson-smile"));
49+
this(Jackson2ObjectMapperBuilder.smile().build(), DEFAULT_SMILE_MIME_TYPES);
4750
}
4851

4952
public Jackson2SmileEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
5053
super(mapper, mimeTypes);
5154
Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass());
5255
this.streamingMediaTypes.add(new MediaType("application", "stream+x-jackson-smile"));
53-
}
54-
55-
56-
@Override
57-
public List<MimeType> getEncodableMimeTypes() {
58-
return Collections.singletonList(SMILE_MIME_TYPE);
56+
this.streamingLineSeparator = false;
5957
}
6058

6159
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ private Flux<TokenBuffer> parseTokenBufferFlux() throws IOException {
125125

126126
while (true) {
127127
JsonToken token = this.parser.nextToken();
128-
if (token == null || token == JsonToken.NOT_AVAILABLE) {
128+
// SPR-16151: Smile data format uses null to separate documents
129+
if ((token == JsonToken.NOT_AVAILABLE) ||
130+
(token == null && (token = this.parser.nextToken()) == null)) {
129131
break;
130132
}
131133
updateDepth(token);

spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -52,6 +52,8 @@
5252
import static org.junit.Assert.assertTrue;
5353
import static org.springframework.core.ResolvableType.forClass;
5454
import static org.springframework.http.MediaType.APPLICATION_JSON;
55+
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
56+
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
5557
import static org.springframework.http.MediaType.APPLICATION_XML;
5658
import static org.springframework.http.codec.json.Jackson2JsonDecoder.JSON_VIEW_HINT;
5759
import static org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
@@ -70,6 +72,8 @@ public void canDecode() {
7072
Jackson2JsonDecoder decoder = new Jackson2JsonDecoder();
7173

7274
assertTrue(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON));
75+
assertTrue(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON_UTF8));
76+
assertTrue(decoder.canDecode(forClass(Pojo.class), APPLICATION_STREAM_JSON));
7377
assertTrue(decoder.canDecode(forClass(Pojo.class), null));
7478

7579
assertFalse(decoder.canDecode(forClass(String.class), null));
@@ -130,7 +134,7 @@ public void decodeToList() throws Exception {
130134
}
131135

132136
@Test
133-
public void decodeToFlux() throws Exception {
137+
public void decodeArrayToFlux() throws Exception {
134138
Flux<DataBuffer> source = Flux.just(stringBuffer(
135139
"[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"));
136140

@@ -144,6 +148,21 @@ public void decodeToFlux() throws Exception {
144148
.verifyComplete();
145149
}
146150

151+
@Test
152+
public void decodeStreamToFlux() throws Exception {
153+
Flux<DataBuffer> source = Flux.just(stringBuffer("{\"bar\":\"b1\",\"foo\":\"f1\"}"),
154+
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}"));
155+
156+
ResolvableType elementType = forClass(Pojo.class);
157+
Flux<Object> flux = new Jackson2JsonDecoder().decode(source, elementType, APPLICATION_STREAM_JSON,
158+
emptyMap());
159+
160+
StepVerifier.create(flux)
161+
.expectNext(new Pojo("f1", "b1"))
162+
.expectNext(new Pojo("f2", "b2"))
163+
.verifyComplete();
164+
}
165+
147166
@Test
148167
public void decodeEmptyArrayToFlux() throws Exception {
149168
Flux<DataBuffer> source = Flux.just(stringBuffer("[]"));

spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -57,6 +57,8 @@ public class Jackson2JsonEncoderTests extends AbstractDataBufferAllocatingTestCa
5757
public void canEncode() {
5858
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
5959
assertTrue(this.encoder.canEncode(pojoType, APPLICATION_JSON));
60+
assertTrue(this.encoder.canEncode(pojoType, APPLICATION_JSON_UTF8));
61+
assertTrue(this.encoder.canEncode(pojoType, APPLICATION_STREAM_JSON));
6062
assertTrue(this.encoder.canEncode(pojoType, null));
6163

6264
// SPR-15464

spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -38,6 +38,7 @@
3838
import static org.junit.Assert.assertTrue;
3939
import static org.springframework.core.ResolvableType.forClass;
4040
import static org.springframework.http.MediaType.APPLICATION_JSON;
41+
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
4142

4243
/**
4344
* Unit tests for {@link Jackson2SmileDecoder}.
@@ -47,12 +48,14 @@
4748
public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestCase {
4849

4950
private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile");
51+
private final static MimeType STREAM_SMILE_MIME_TYPE = new MimeType("application", "stream+x-jackson-smile");
5052

5153
private final Jackson2SmileDecoder decoder = new Jackson2SmileDecoder();
5254

5355
@Test
5456
public void canDecode() {
5557
assertTrue(decoder.canDecode(forClass(Pojo.class), SMILE_MIME_TYPE));
58+
assertTrue(decoder.canDecode(forClass(Pojo.class), STREAM_SMILE_MIME_TYPE));
5659
assertTrue(decoder.canDecode(forClass(Pojo.class), null));
5760

5861
assertFalse(decoder.canDecode(forClass(String.class), null));
@@ -100,7 +103,7 @@ public void decodeToList() throws Exception {
100103
}
101104

102105
@Test
103-
public void decodeToFlux() throws Exception {
106+
public void decodeListToFlux() throws Exception {
104107
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
105108
List<Pojo> list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"));
106109
byte[] serializedList = mapper.writer().writeValueAsBytes(list);
@@ -115,4 +118,20 @@ public void decodeToFlux() throws Exception {
115118
.verifyComplete();
116119
}
117120

121+
@Test
122+
public void decodeStreamToFlux() throws Exception {
123+
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
124+
List<Pojo> list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"));
125+
byte[] serializedList = mapper.writer().writeValueAsBytes(list);
126+
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedList));
127+
128+
ResolvableType elementType = forClass(Pojo.class);
129+
Flux<Object> flux = decoder.decode(source, elementType, STREAM_SMILE_MIME_TYPE, emptyMap());
130+
131+
StepVerifier.create(flux)
132+
.expectNext(new Pojo("f1", "b1"))
133+
.expectNext(new Pojo("f2", "b2"))
134+
.verifyComplete();
135+
}
136+
118137
}

0 commit comments

Comments
 (0)