Browse Source

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

(cherry picked from commit 79c339b03e)
pull/25714/head
Arjen Poutsma 6 years ago
parent
commit
f4ae18fa89
  1. 3
      spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java
  2. 20
      spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
  3. 26
      spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java
  4. 17
      spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java
  5. 40
      spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java

3
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.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]); 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()) { for (JsonEncoding encoding : JsonEncoding.values()) {
ENCODINGS.put(encoding.getJavaName(), encoding); ENCODINGS.put(encoding.getJavaName(), encoding);
} }
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
} }

20
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.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference; 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.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
@ -76,7 +74,16 @@ import org.springframework.util.TypeUtils;
*/ */
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
private static final Map<String, JsonEncoding> ENCODINGS = jsonEncodings(); private static final Map<String, JsonEncoding> 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. * The default charset used by the converter.
@ -398,9 +405,4 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return super.getContentLength(object, contentType); return super.getContentLength(object, contentType);
} }
private static Map<String, JsonEncoding> jsonEncodings() {
return EnumSet.allOf(JsonEncoding.class).stream()
.collect(Collectors.toMap(JsonEncoding::getJavaName, Function.identity()));
}
} }

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

@ -91,6 +91,8 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTestCase<Jackson2Js
assertFalse(decoder.canDecode(forClass(Pojo.class), APPLICATION_XML)); assertFalse(decoder.canDecode(forClass(Pojo.class), APPLICATION_XML));
assertTrue(this.decoder.canDecode(forClass(Pojo.class), assertTrue(this.decoder.canDecode(forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.UTF_8))); new MediaType("application", "json", StandardCharsets.UTF_8)));
assertTrue(this.decoder.canDecode(forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.US_ASCII)));
assertTrue(this.decoder.canDecode(forClass(Pojo.class), assertTrue(this.decoder.canDecode(forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.ISO_8859_1))); new MediaType("application", "json", StandardCharsets.ISO_8859_1)));
@ -246,8 +248,7 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTestCase<Jackson2Js
stringBuffer("{\"føø\":\"bår\"}", StandardCharsets.ISO_8859_1) stringBuffer("{\"føø\":\"bår\"}", StandardCharsets.ISO_8859_1)
); );
testDecode(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() { testDecode(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {}),
}),
step -> step.assertNext(o -> { step -> step.assertNext(o -> {
assertTrue(o instanceof Map); assertTrue(o instanceof Map);
Map<String, String> map = (Map<String, String>) o; Map<String, String> map = (Map<String, String>) o;
@ -263,8 +264,7 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTestCase<Jackson2Js
public void decodeMonoNonUtf8Encoding() { public void decodeMonoNonUtf8Encoding() {
Mono<DataBuffer> input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16); Mono<DataBuffer> input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16);
testDecodeToMono(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() { testDecodeToMono(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {}),
}),
step -> step.assertNext(o -> { step -> step.assertNext(o -> {
Map<String, String> map = (Map<String, String>) o; Map<String, String> map = (Map<String, String>) o;
assertEquals("bar", map.get("foo")); assertEquals("bar", map.get("foo"));
@ -274,6 +274,24 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTestCase<Jackson2Js
null); null);
} }
@Test
@SuppressWarnings("unchecked")
public void decodeAscii() {
Flux<DataBuffer> input = Flux.concat(
stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.US_ASCII)
);
testDecode(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {}),
step -> step.assertNext(o -> {
Map<String, String> map = (Map<String, String>) o;
assertEquals("bar", map.get("foo"));
})
.verifyComplete(),
MediaType.parseMediaType("application/json; charset=us-ascii"),
null);
}
private Mono<DataBuffer> stringBuffer(String value) { private Mono<DataBuffer> stringBuffer(String value) {
return stringBuffer(value, StandardCharsets.UTF_8); return stringBuffer(value, StandardCharsets.UTF_8);
} }

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

@ -42,7 +42,9 @@ import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
@ -73,6 +75,8 @@ public class Jackson2JsonEncoderTests extends AbstractEncoderTestCase<Jackson2Js
assertTrue(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), assertTrue(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.UTF_8))); new MediaType("application", "json", StandardCharsets.UTF_8)));
assertTrue(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.US_ASCII)));
assertFalse(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), assertFalse(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.ISO_8859_1))); new MediaType("application", "json", StandardCharsets.ISO_8859_1)));
@ -223,6 +227,17 @@ public class Jackson2JsonEncoderTests extends AbstractEncoderTestCase<Jackson2Js
.verify(Duration.ofSeconds(5)); .verify(Duration.ofSeconds(5));
} }
@Test
public void encodeAscii() {
Mono<Object> 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") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
private static class ParentClass { private static class ParentClass {

40
spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java

@ -43,8 +43,14 @@ import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/** /**
* Jackson 2.x converter tests. * Jackson 2.x converter tests.
@ -65,6 +71,7 @@ public class MappingJackson2HttpMessageConverterTests {
assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json"))); assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json")));
assertTrue(converter.canRead(Map.class, new MediaType("application", "json"))); assertTrue(converter.canRead(Map.class, new MediaType("application", "json")));
assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))); assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8)));
assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII)));
assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))); assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1)));
} }
@ -73,6 +80,7 @@ public class MappingJackson2HttpMessageConverterTests {
assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json"))); assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json")));
assertTrue(converter.canWrite(Map.class, new MediaType("application", "json"))); assertTrue(converter.canWrite(Map.class, new MediaType("application", "json")));
assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))); assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8)));
assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII)));
assertFalse(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))); assertFalse(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1)));
} }
@ -465,6 +473,34 @@ public class MappingJackson2HttpMessageConverterTests {
assertEquals("bår", result.get("føø")); assertEquals("bår", result.get("føø"));
} }
@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<String, Object> result = (HashMap<String, Object>) this.converter.read(HashMap.class, inputMessage);
assertEquals(1, result.size());
assertEquals("bar", result.get("foo"));
}
@Test
@SuppressWarnings("unchecked")
public void writeAscii() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
Map<String,Object> 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);
assertEquals("{\"foo\":\"bar\"}", result);
assertEquals(contentType, outputMessage.getHeaders().getContentType());
}
interface MyInterface { interface MyInterface {

Loading…
Cancel
Save