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)
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user