Skip to content

Commit 9c3417f

Browse files
committed
Convert non-unicode input when reading w/ Jackson
This commit makes sure that Jackson-based message converters and decoders can deal with non-unicode input. It does so by reading non-unicode input messages with a InputStreamReader. This commit also adds additional tests forthe canRead/canWrite methods on both codecs and message converters. Closes: gh-25247
1 parent fd7e648 commit 9c3417f

11 files changed

+113
-100
lines changed

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,18 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
6969

7070
private static final Map<MediaType, byte[]> STREAM_SEPARATORS;
7171

72+
private static final Map<Charset, JsonEncoding> ENCODINGS;
73+
7274
static {
7375
STREAM_SEPARATORS = new HashMap<>(4);
7476
STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
7577
STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]);
78+
79+
ENCODINGS = new HashMap<>(JsonEncoding.values().length);
80+
for (JsonEncoding encoding : JsonEncoding.values()) {
81+
Charset charset = Charset.forName(encoding.getJavaName());
82+
ENCODINGS.put(charset, encoding);
83+
}
7684
}
7785

7886

@@ -103,7 +111,16 @@ public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
103111
@Override
104112
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
105113
Class<?> clazz = elementType.toClass();
106-
return supportsMimeType(mimeType) && (Object.class == clazz ||
114+
if (!supportsMimeType(mimeType)) {
115+
return false;
116+
}
117+
if (mimeType != null && mimeType.getCharset() != null) {
118+
Charset charset = mimeType.getCharset();
119+
if (!ENCODINGS.containsKey(charset)) {
120+
return false;
121+
}
122+
}
123+
return (Object.class == clazz ||
107124
(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
108125
}
109126

@@ -270,10 +287,9 @@ private byte[] streamSeparator(@Nullable MimeType mimeType) {
270287
protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
271288
if (mimeType != null && mimeType.getCharset() != null) {
272289
Charset charset = mimeType.getCharset();
273-
for (JsonEncoding encoding : JsonEncoding.values()) {
274-
if (charset.name().equals(encoding.getJavaName())) {
275-
return encoding;
276-
}
290+
JsonEncoding result = ENCODINGS.get(charset);
291+
if (result != null) {
292+
return result;
277293
}
278294
}
279295
return JsonEncoding.UTF8;

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

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,13 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.Type;
21-
import java.nio.charset.Charset;
2221
import java.util.Arrays;
2322
import java.util.Collections;
24-
import java.util.EnumSet;
2523
import java.util.HashMap;
2624
import java.util.List;
2725
import java.util.Map;
28-
import java.util.function.Function;
29-
import java.util.stream.Collectors;
3026

3127
import com.fasterxml.jackson.annotation.JsonView;
32-
import com.fasterxml.jackson.core.JsonEncoding;
3328
import com.fasterxml.jackson.databind.JavaType;
3429
import com.fasterxml.jackson.databind.ObjectMapper;
3530
import com.fasterxml.jackson.databind.type.TypeFactory;
@@ -80,9 +75,6 @@ public abstract class Jackson2CodecSupport {
8075
new MimeType("application", "json"),
8176
new MimeType("application", "*+json")));
8277

83-
private static final Map<String, JsonEncoding> ENCODINGS = jsonEncodings();
84-
85-
8678

8779
protected final Log logger = HttpLogging.forLogName(getClass());
8880

@@ -115,17 +107,7 @@ protected List<MimeType> getMimeTypes() {
115107

116108

117109
protected boolean supportsMimeType(@Nullable MimeType mimeType) {
118-
if (mimeType == null) {
119-
return true;
120-
}
121-
else if (this.mimeTypes.stream().noneMatch(m -> m.isCompatibleWith(mimeType))) {
122-
return false;
123-
}
124-
else if (mimeType.getCharset() != null) {
125-
Charset charset = mimeType.getCharset();
126-
return ENCODINGS.containsKey(charset.name());
127-
}
128-
return true;
110+
return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
129111
}
130112

131113
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
@@ -163,10 +145,4 @@ protected MethodParameter getParameter(ResolvableType type) {
163145
@Nullable
164146
protected abstract <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType);
165147

166-
private static Map<String, JsonEncoding> jsonEncodings() {
167-
return EnumSet.allOf(JsonEncoding.class).stream()
168-
.collect(Collectors.toMap(JsonEncoding::getJavaName, Function.identity()));
169-
}
170-
171-
172148
}

spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package org.springframework.http.converter.json;
1818

1919
import java.io.IOException;
20+
import java.io.InputStreamReader;
21+
import java.io.Reader;
2022
import java.lang.reflect.Type;
2123
import java.nio.charset.Charset;
24+
import java.nio.charset.StandardCharsets;
2225
import java.util.Arrays;
2326
import java.util.Collections;
2427
import java.util.EnumSet;
@@ -36,6 +39,7 @@
3639
import com.fasterxml.jackson.databind.JavaType;
3740
import com.fasterxml.jackson.databind.JsonMappingException;
3841
import com.fasterxml.jackson.databind.ObjectMapper;
42+
import com.fasterxml.jackson.databind.ObjectReader;
3943
import com.fasterxml.jackson.databind.ObjectWriter;
4044
import com.fasterxml.jackson.databind.SerializationConfig;
4145
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -72,7 +76,7 @@
7276
*/
7377
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
7478

75-
private static final Map<String, JsonEncoding> ENCODINGS = jsonEncodings();
79+
private static final Map<Charset, JsonEncoding> ENCODINGS = jsonEncodings();
7680

7781
/**
7882
* The default charset used by the converter.
@@ -173,19 +177,17 @@ public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable Med
173177
return false;
174178
}
175179

176-
@Override
177-
protected boolean canRead(@Nullable MediaType mediaType) {
178-
if (!super.canRead(mediaType)) {
179-
return false;
180-
}
181-
return checkEncoding(mediaType);
182-
}
183-
184180
@Override
185181
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
186182
if (!canWrite(mediaType)) {
187183
return false;
188184
}
185+
if (mediaType != null && mediaType.getCharset() != null) {
186+
Charset charset = mediaType.getCharset();
187+
if (!ENCODINGS.containsKey(charset)) {
188+
return false;
189+
}
190+
}
189191
AtomicReference<Throwable> causeRef = new AtomicReference<>();
190192
if (this.objectMapper.canSerialize(clazz, causeRef)) {
191193
return true;
@@ -194,14 +196,6 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
194196
return false;
195197
}
196198

197-
@Override
198-
protected boolean canWrite(@Nullable MediaType mediaType) {
199-
if (!super.canWrite(mediaType)) {
200-
return false;
201-
}
202-
return checkEncoding(mediaType);
203-
}
204-
205199
/**
206200
* Determine whether to log the given exception coming from a
207201
* {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
@@ -233,14 +227,6 @@ else if (logger.isDebugEnabled()) {
233227
}
234228
}
235229

236-
private boolean checkEncoding(@Nullable MediaType mediaType) {
237-
if (mediaType != null && mediaType.getCharset() != null) {
238-
Charset charset = mediaType.getCharset();
239-
return ENCODINGS.containsKey(charset.name());
240-
}
241-
return true;
242-
}
243-
244230
@Override
245231
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
246232
throws IOException, HttpMessageNotReadableException {
@@ -258,15 +244,31 @@ public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage
258244
}
259245

260246
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
247+
MediaType contentType = inputMessage.getHeaders().getContentType();
248+
Charset charset = getCharset(contentType);
249+
250+
boolean isUnicode = ENCODINGS.containsKey(charset);
261251
try {
262252
if (inputMessage instanceof MappingJacksonInputMessage) {
263253
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
264254
if (deserializationView != null) {
265-
return this.objectMapper.readerWithView(deserializationView).forType(javaType).
266-
readValue(inputMessage.getBody());
255+
ObjectReader objectReader = this.objectMapper.readerWithView(deserializationView).forType(javaType);
256+
if (isUnicode) {
257+
return objectReader.readValue(inputMessage.getBody());
258+
}
259+
else {
260+
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
261+
return objectReader.readValue(reader);
262+
}
267263
}
268264
}
269-
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
265+
if (isUnicode) {
266+
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
267+
}
268+
else {
269+
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
270+
return this.objectMapper.readValue(reader, javaType);
271+
}
270272
}
271273
catch (InvalidDefinitionException ex) {
272274
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
@@ -276,6 +278,15 @@ private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) th
276278
}
277279
}
278280

281+
private static Charset getCharset(@Nullable MediaType contentType) {
282+
if (contentType != null && contentType.getCharset() != null) {
283+
return contentType.getCharset();
284+
}
285+
else {
286+
return StandardCharsets.UTF_8;
287+
}
288+
}
289+
279290
@Override
280291
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
281292
throws IOException, HttpMessageNotWritableException {
@@ -363,7 +374,7 @@ protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
363374
protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) {
364375
if (contentType != null && contentType.getCharset() != null) {
365376
Charset charset = contentType.getCharset();
366-
JsonEncoding encoding = ENCODINGS.get(charset.name());
377+
JsonEncoding encoding = ENCODINGS.get(charset);
367378
if (encoding != null) {
368379
return encoding;
369380
}
@@ -388,9 +399,9 @@ protected Long getContentLength(Object object, @Nullable MediaType contentType)
388399
return super.getContentLength(object, contentType);
389400
}
390401

391-
private static Map<String, JsonEncoding> jsonEncodings() {
402+
private static Map<Charset, JsonEncoding> jsonEncodings() {
392403
return EnumSet.allOf(JsonEncoding.class).stream()
393-
.collect(Collectors.toMap(JsonEncoding::getJavaName, Function.identity()));
404+
.collect(Collectors.toMap(encoding -> Charset.forName(encoding.getJavaName()), Function.identity()));
394405
}
395406

396407
}

spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborDecoderTests.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

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

19-
import java.nio.charset.StandardCharsets;
2019
import java.util.Arrays;
2120
import java.util.List;
2221

@@ -28,7 +27,6 @@
2827
import org.springframework.core.ResolvableType;
2928
import org.springframework.core.io.buffer.DataBuffer;
3029
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
31-
import org.springframework.http.MediaType;
3230
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
3331
import org.springframework.util.MimeType;
3432
import org.springframework.web.testfixture.xml.Pojo;
@@ -64,11 +62,6 @@ public void canDecode() {
6462

6563
assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
6664
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isFalse();
67-
68-
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
69-
new MediaType("application", "cbor", StandardCharsets.UTF_8))).isTrue();
70-
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
71-
new MediaType("application", "cbor", StandardCharsets.ISO_8859_1))).isFalse();
7265
}
7366

7467
@Override

spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborEncoderTests.java

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

1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
21-
import java.nio.charset.StandardCharsets;
2221
import java.util.function.Consumer;
2322

2423
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -29,7 +28,6 @@
2928
import org.springframework.core.io.buffer.DataBuffer;
3029
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
3130
import org.springframework.core.testfixture.io.buffer.DataBufferTestUtils;
32-
import org.springframework.http.MediaType;
3331
import org.springframework.http.codec.ServerSentEvent;
3432
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
3533
import org.springframework.util.MimeType;
@@ -75,12 +73,6 @@ public void canEncode() {
7573

7674
// SPR-15464
7775
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();
78-
79-
80-
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
81-
new MediaType("application", "cbor", StandardCharsets.UTF_8))).isTrue();
82-
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
83-
new MediaType("application", "cbor", StandardCharsets.ISO_8859_1))).isFalse();
8476
}
8577

8678
@Test

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public void canDecode() {
8888
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
8989
new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue();
9090
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
91-
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isFalse();
91+
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue();
9292
}
9393

9494
@Test // SPR-15866
@@ -235,6 +235,21 @@ public void decodeNonUtf8Encoding() {
235235
null);
236236
}
237237

238+
@Test
239+
@SuppressWarnings("unchecked")
240+
public void decodeNonUnicode() {
241+
Flux<DataBuffer> input = Flux.concat(
242+
stringBuffer("{\"føø\":\"bår\"}", StandardCharsets.ISO_8859_1)
243+
);
244+
245+
testDecode(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {
246+
}),
247+
step -> step.assertNext(o -> assertThat((Map<String, String>) o).containsEntry("føø", "bår"))
248+
.verifyComplete(),
249+
MediaType.parseMediaType("application/json; charset=iso-8859-1"),
250+
null);
251+
}
252+
238253
@Test
239254
@SuppressWarnings("unchecked")
240255
public void decodeMonoNonUtf8Encoding() {

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

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

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

19-
import java.nio.charset.StandardCharsets;
2019
import java.util.Arrays;
2120
import java.util.List;
2221

@@ -28,7 +27,6 @@
2827
import org.springframework.core.ResolvableType;
2928
import org.springframework.core.io.buffer.DataBuffer;
3029
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
31-
import org.springframework.http.MediaType;
3230
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
3331
import org.springframework.util.MimeType;
3432
import org.springframework.web.testfixture.xml.Pojo;
@@ -65,12 +63,6 @@ public void canDecode() {
6563

6664
assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
6765
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isFalse();
68-
69-
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
70-
new MediaType("application", "x-jackson-smile", StandardCharsets.UTF_8))).isTrue();
71-
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
72-
new MediaType("application", "x-jackson-smile", StandardCharsets.ISO_8859_1))).isFalse();
73-
7466
}
7567

7668
@Override

0 commit comments

Comments
 (0)