Skip to content

Commit 439ffe2

Browse files
committed
Convert non-UTF-8 JSON
Jackson's asynchronous parser does not support any encoding except UTF-8 (or ASCII). This commit converts non-UTF-8/ASCII encoded JSON to UTF-8. Closes gh-24489
1 parent 4e55262 commit 439ffe2

File tree

3 files changed

+101
-4
lines changed

3 files changed

+101
-4
lines changed

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,29 @@ public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementTy
120120
forceUseOfBigDecimal = true;
121121
}
122122

123-
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(),
123+
Flux<DataBuffer> processed = processInput(input, elementType, mimeType, hints);
124+
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(processed, this.jsonFactory, getObjectMapper(),
124125
true, forceUseOfBigDecimal, getMaxInMemorySize());
125126
return decodeInternal(tokens, elementType, mimeType, hints);
126127
}
127128

129+
/**
130+
* Process the input publisher into a flux. Default implementation returns
131+
* {@link Flux#from(Publisher)}, but subclasses can choose to to customize
132+
* this behaviour.
133+
* @param input the {@code DataBuffer} input stream to process
134+
* @param elementType the expected type of elements in the output stream
135+
* @param mimeType the MIME type associated with the input stream (optional)
136+
* @param hints additional information about how to do encode
137+
* @return the processed flux
138+
* @since 5.1.14
139+
*/
140+
protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType,
141+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
142+
143+
return Flux.from(input);
144+
}
145+
128146
@Override
129147
public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
130148
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
@@ -134,7 +152,8 @@ public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType ele
134152
forceUseOfBigDecimal = true;
135153
}
136154

137-
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(),
155+
Flux<DataBuffer> processed = processInput(input, elementType, mimeType, hints);
156+
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(processed, this.jsonFactory, getObjectMapper(),
138157
false, forceUseOfBigDecimal, getMaxInMemorySize());
139158
return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty();
140159
}

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 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,10 +16,24 @@
1616

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

19+
import java.nio.charset.Charset;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Arrays;
22+
import java.util.Map;
23+
1924
import com.fasterxml.jackson.databind.ObjectMapper;
25+
import org.reactivestreams.Publisher;
26+
import reactor.core.publisher.Flux;
2027

28+
import org.springframework.core.ResolvableType;
29+
import org.springframework.core.codec.StringDecoder;
30+
import org.springframework.core.io.buffer.DataBuffer;
31+
import org.springframework.core.io.buffer.DataBufferFactory;
32+
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
2133
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
34+
import org.springframework.lang.Nullable;
2235
import org.springframework.util.MimeType;
36+
import org.springframework.util.MimeTypeUtils;
2337

2438
/**
2539
* Decode a byte stream into JSON and convert to Object's with Jackson 2.9,
@@ -32,6 +46,11 @@
3246
*/
3347
public class Jackson2JsonDecoder extends AbstractJackson2Decoder {
3448

49+
private static final StringDecoder STRING_DECODER = StringDecoder.textPlainOnly(Arrays.asList(",", "\n"), false);
50+
51+
private static final ResolvableType STRING_TYPE = ResolvableType.forClass(String.class);
52+
53+
3554
public Jackson2JsonDecoder() {
3655
super(Jackson2ObjectMapperBuilder.json().build());
3756
}
@@ -40,4 +59,28 @@ public Jackson2JsonDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
4059
super(mapper, mimeTypes);
4160
}
4261

62+
@Override
63+
protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType,
64+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
65+
66+
Flux<DataBuffer> flux = Flux.from(input);
67+
if (mimeType == null) {
68+
return flux;
69+
}
70+
71+
// Jackson asynchronous parser only supports UTF-8
72+
Charset charset = mimeType.getCharset();
73+
if (charset == null || StandardCharsets.UTF_8.equals(charset) || StandardCharsets.US_ASCII.equals(charset)) {
74+
return flux;
75+
}
76+
77+
// Potentially, the memory consumption of this conversion could be improved by using CharBuffers instead
78+
// of allocating Strings, but that would require refactoring the buffer tokenization code from StringDecoder
79+
80+
MimeType textMimeType = new MimeType(MimeTypeUtils.TEXT_PLAIN, charset);
81+
Flux<String> decoded = STRING_DECODER.decode(input, STRING_TYPE, textMimeType, null);
82+
DataBufferFactory factory = new DefaultDataBufferFactory();
83+
return decoded.map(s -> factory.wrap(s.getBytes(StandardCharsets.UTF_8)));
84+
}
85+
4386
}

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.math.BigDecimal;
21+
import java.nio.charset.Charset;
2122
import java.nio.charset.StandardCharsets;
2223
import java.util.Collections;
2324
import java.util.List;
@@ -34,6 +35,7 @@
3435
import reactor.core.publisher.Mono;
3536
import reactor.test.StepVerifier;
3637

38+
import org.springframework.core.ParameterizedTypeReference;
3739
import org.springframework.core.ResolvableType;
3840
import org.springframework.core.codec.AbstractDecoderTestCase;
3941
import org.springframework.core.codec.CodecException;
@@ -218,9 +220,42 @@ public void bigDecimalFlux() {
218220
);
219221
}
220222

223+
@Test
224+
public void decodeNonUtf8Encoding() {
225+
Mono<DataBuffer> input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16);
226+
227+
testDecode(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {}),
228+
step -> step.assertNext(o -> {
229+
Map<String, String> map = (Map<String, String>) o;
230+
assertEquals("bar", map.get("foo"));
231+
})
232+
.verifyComplete(),
233+
MediaType.parseMediaType("application/json; charset=utf-16"),
234+
null);
235+
}
236+
237+
@Test
238+
public void decodeMonoNonUtf8Encoding() {
239+
Mono<DataBuffer> input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16);
240+
241+
testDecodeToMono(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {
242+
}),
243+
step -> step.assertNext(o -> {
244+
Map<String, String> map = (Map<String, String>) o;
245+
assertEquals("bar", map.get("foo"));
246+
})
247+
.verifyComplete(),
248+
MediaType.parseMediaType("application/json; charset=utf-16"),
249+
null);
250+
}
251+
221252
private Mono<DataBuffer> stringBuffer(String value) {
253+
return stringBuffer(value, StandardCharsets.UTF_8);
254+
}
255+
256+
private Mono<DataBuffer> stringBuffer(String value, Charset charset) {
222257
return Mono.defer(() -> {
223-
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
258+
byte[] bytes = value.getBytes(charset);
224259
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
225260
buffer.write(bytes);
226261
return Mono.just(buffer);

0 commit comments

Comments
 (0)