From 79c339b03e4fca4d58598207cf8e6d8c2c54652d Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 30 Jun 2020 15:46:54 +0200 Subject: [PATCH] Support for ASCII in Jackson codec & converter This commit introduces support for writing JSON with an US-ASCII character encoding in the Jackson encoder and message converter, treating it like UTF-8. See gh-25322 --- .../codec/json/AbstractJackson2Encoder.java | 3 +- .../AbstractJackson2HttpMessageConverter.java | 20 +++++++------ .../codec/json/Jackson2JsonDecoderTests.java | 26 +++++++++++++---- .../codec/json/Jackson2JsonEncoderTests.java | 13 +++++++++ ...pingJackson2HttpMessageConverterTests.java | 29 +++++++++++++++++++ 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index 0437c5cbb3..53a9009781 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -76,10 +76,11 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR); STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]); - ENCODINGS = new HashMap<>(JsonEncoding.values().length); + ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1); for (JsonEncoding encoding : JsonEncoding.values()) { ENCODINGS.put(encoding.getJavaName(), encoding); } + ENCODINGS.put("US-ASCII", JsonEncoding.UTF8); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index d52b376ae8..8a6d202300 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -24,11 +24,9 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; -import java.util.EnumSet; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; @@ -76,7 +74,16 @@ import org.springframework.util.TypeUtils; */ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter { - private static final Map ENCODINGS = jsonEncodings(); + private static final Map ENCODINGS; + + static { + ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1); + for (JsonEncoding encoding : JsonEncoding.values()) { + ENCODINGS.put(encoding.getJavaName(), encoding); + } + ENCODINGS.put("US-ASCII", JsonEncoding.UTF8); + } + /** * The default charset used by the converter. @@ -399,9 +406,4 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return super.getContentLength(object, contentType); } - private static Map jsonEncodings() { - return EnumSet.allOf(JsonEncoding.class).stream() - .collect(Collectors.toMap(JsonEncoding::getJavaName, Function.identity())); - } - } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index 2b8100934d..6393f39cb4 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -87,6 +87,8 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTests input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16); - testDecode(input, ResolvableType.forType(new ParameterizedTypeReference>() { - }), + testDecode(input, ResolvableType.forType(new ParameterizedTypeReference>() {}), step -> step.assertNext(o -> assertThat((Map) o).containsEntry("foo", "bar")) .verifyComplete(), MediaType.parseMediaType("application/json; charset=utf-16"), @@ -242,8 +243,7 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTests>() { - }), + testDecode(input, ResolvableType.forType(new ParameterizedTypeReference>() {}), step -> step.assertNext(o -> assertThat((Map) o).containsEntry("føø", "bår")) .verifyComplete(), MediaType.parseMediaType("application/json; charset=iso-8859-1"), @@ -255,14 +255,28 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTests input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16); - testDecodeToMono(input, ResolvableType.forType(new ParameterizedTypeReference>() { - }), + testDecodeToMono(input, ResolvableType.forType(new ParameterizedTypeReference>() {}), step -> step.assertNext(o -> assertThat((Map) o).containsEntry("foo", "bar")) .verifyComplete(), MediaType.parseMediaType("application/json; charset=utf-16"), null); } + @Test + @SuppressWarnings("unchecked") + public void decodeAscii() { + Flux input = Flux.concat( + stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.US_ASCII) + ); + + testDecode(input, ResolvableType.forType(new ParameterizedTypeReference>() {}), + step -> step.assertNext(o -> assertThat((Map) o).containsEntry("foo", "bar")) + .verifyComplete(), + MediaType.parseMediaType("application/json; charset=us-ascii"), + null); + } + + private Mono stringBuffer(String value) { return stringBuffer(value, StandardCharsets.UTF_8); } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java index 256d95220e..b62897e65c 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java @@ -71,6 +71,8 @@ public class Jackson2JsonEncoderTests extends AbstractEncoderTests input = Mono.just(new Pojo("foo", "bar")); + + testEncode(input, ResolvableType.forClass(Pojo.class), step -> step + .consumeNextWith(expectString("{\"foo\":\"foo\",\"bar\":\"bar\"}")) + .verifyComplete(), + new MimeType("application", "json", StandardCharsets.US_ASCII), null); + + } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") private static class ParentClass { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index c08ace1907..bb5d890fb6 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -67,6 +67,7 @@ public class MappingJackson2HttpMessageConverterTests { assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canRead(Map.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue(); + assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue(); assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue(); } @@ -75,6 +76,7 @@ public class MappingJackson2HttpMessageConverterTests { assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canWrite(Map.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue(); + assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isFalse(); } @@ -460,6 +462,33 @@ public class MappingJackson2HttpMessageConverterTests { assertThat(result).containsExactly(entry("føø", "bår")); } + @Test + @SuppressWarnings("unchecked") + public void readAscii() throws Exception { + String body = "{\"foo\":\"bar\"}"; + Charset charset = StandardCharsets.US_ASCII; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); + inputMessage.getHeaders().setContentType(new MediaType("application", "json", charset)); + HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); + + assertThat(result).containsExactly(entry("foo", "bar")); + } + + @Test + @SuppressWarnings("unchecked") + public void writeAscii() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Map body = new HashMap<>(); + body.put("foo", "bar"); + Charset charset = StandardCharsets.US_ASCII; + MediaType contentType = new MediaType("application", "json", charset); + converter.write(body, contentType, outputMessage); + + String result = outputMessage.getBodyAsString(charset); + assertThat(result).isEqualTo("{\"foo\":\"bar\"}"); + assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType); + } + interface MyInterface {