Skip to content

Commit

Permalink
Convert non-unicode input when reading w/ Jackson
Browse files Browse the repository at this point in the history
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: spring-projectsgh-25247
  • Loading branch information
poutsma committed Jun 15, 2020
1 parent 94d7462 commit 525c8b1
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 100 deletions.
Expand Up @@ -69,10 +69,18 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple

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

private static final Map<Charset, JsonEncoding> ENCODINGS;

static {
STREAM_SEPARATORS = new HashMap<>(4);
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);
for (JsonEncoding encoding : JsonEncoding.values()) {
Charset charset = Charset.forName(encoding.getJavaName());
ENCODINGS.put(charset, encoding);
}
}


Expand Down Expand Up @@ -103,7 +111,16 @@ public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
@Override
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
Class<?> clazz = elementType.toClass();
return supportsMimeType(mimeType) && (Object.class == clazz ||
if (!supportsMimeType(mimeType)) {
return false;
}
if (mimeType != null && mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
if (!ENCODINGS.containsKey(charset)) {
return false;
}
}
return (Object.class == clazz ||
(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
}

Expand Down Expand Up @@ -270,10 +287,9 @@ private byte[] streamSeparator(@Nullable MimeType mimeType) {
protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
if (mimeType != null && mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
for (JsonEncoding encoding : JsonEncoding.values()) {
if (charset.name().equals(encoding.getJavaName())) {
return encoding;
}
JsonEncoding result = ENCODINGS.get(charset);
if (result != null) {
return result;
}
}
return JsonEncoding.UTF8;
Expand Down
Expand Up @@ -18,18 +18,13 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
Expand Down Expand Up @@ -80,9 +75,6 @@ public abstract class Jackson2CodecSupport {
new MimeType("application", "json"),
new MimeType("application", "*+json")));

private static final Map<String, JsonEncoding> ENCODINGS = jsonEncodings();



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

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


protected boolean supportsMimeType(@Nullable MimeType mimeType) {
if (mimeType == null) {
return true;
}
else if (this.mimeTypes.stream().noneMatch(m -> m.isCompatibleWith(mimeType))) {
return false;
}
else if (mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
return ENCODINGS.containsKey(charset.name());
}
return true;
return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
}

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

private static Map<String, JsonEncoding> jsonEncodings() {
return EnumSet.allOf(JsonEncoding.class).stream()
.collect(Collectors.toMap(JsonEncoding::getJavaName, Function.identity()));
}


}
Expand Up @@ -17,8 +17,11 @@
package org.springframework.http.converter.json;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
Expand All @@ -36,6 +39,7 @@
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand Down Expand Up @@ -72,7 +76,7 @@
*/
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {

private static final Map<String, JsonEncoding> ENCODINGS = jsonEncodings();
private static final Map<Charset, JsonEncoding> ENCODINGS = jsonEncodings();

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

@Override
protected boolean canRead(@Nullable MediaType mediaType) {
if (!super.canRead(mediaType)) {
return false;
}
return checkEncoding(mediaType);
}

@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
if (!canWrite(mediaType)) {
return false;
}
if (mediaType != null && mediaType.getCharset() != null) {
Charset charset = mediaType.getCharset();
if (!ENCODINGS.containsKey(charset)) {
return false;
}
}
AtomicReference<Throwable> causeRef = new AtomicReference<>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
Expand All @@ -194,14 +196,6 @@ public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
return false;
}

@Override
protected boolean canWrite(@Nullable MediaType mediaType) {
if (!super.canWrite(mediaType)) {
return false;
}
return checkEncoding(mediaType);
}

/**
* Determine whether to log the given exception coming from a
* {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
Expand Down Expand Up @@ -233,14 +227,6 @@ else if (logger.isDebugEnabled()) {
}
}

private boolean checkEncoding(@Nullable MediaType mediaType) {
if (mediaType != null && mediaType.getCharset() != null) {
Charset charset = mediaType.getCharset();
return ENCODINGS.containsKey(charset.name());
}
return true;
}

@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
Expand All @@ -258,15 +244,31 @@ public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage
}

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = getCharset(contentType);

boolean isUnicode = ENCODINGS.containsKey(charset);
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
return this.objectMapper.readerWithView(deserializationView).forType(javaType).
readValue(inputMessage.getBody());
ObjectReader objectReader = this.objectMapper.readerWithView(deserializationView).forType(javaType);
if (isUnicode) {
return objectReader.readValue(inputMessage.getBody());
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return objectReader.readValue(reader);
}
}
}
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
if (isUnicode) {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return this.objectMapper.readValue(reader, javaType);
}
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
Expand All @@ -276,6 +278,15 @@ private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) th
}
}

private static Charset getCharset(@Nullable MediaType contentType) {
if (contentType != null && contentType.getCharset() != null) {
return contentType.getCharset();
}
else {
return StandardCharsets.UTF_8;
}
}

@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Expand Down Expand Up @@ -363,7 +374,7 @@ protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) {
if (contentType != null && contentType.getCharset() != null) {
Charset charset = contentType.getCharset();
JsonEncoding encoding = ENCODINGS.get(charset.name());
JsonEncoding encoding = ENCODINGS.get(charset);
if (encoding != null) {
return encoding;
}
Expand All @@ -388,9 +399,9 @@ protected Long getContentLength(Object object, @Nullable MediaType contentType)
return super.getContentLength(object, contentType);
}

private static Map<String, JsonEncoding> jsonEncodings() {
private static Map<Charset, JsonEncoding> jsonEncodings() {
return EnumSet.allOf(JsonEncoding.class).stream()
.collect(Collectors.toMap(JsonEncoding::getJavaName, Function.identity()));
.collect(Collectors.toMap(encoding -> Charset.forName(encoding.getJavaName()), Function.identity()));
}

}
Expand Up @@ -16,7 +16,6 @@

package org.springframework.http.codec.cbor;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

Expand All @@ -28,7 +27,6 @@
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.MimeType;
import org.springframework.web.testfixture.xml.Pojo;
Expand Down Expand Up @@ -64,11 +62,6 @@ public void canDecode() {

assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isFalse();

assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "cbor", StandardCharsets.UTF_8))).isTrue();
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "cbor", StandardCharsets.ISO_8859_1))).isFalse();
}

@Override
Expand Down
Expand Up @@ -18,7 +18,6 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -29,7 +28,6 @@
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import org.springframework.core.testfixture.io.buffer.DataBufferTestUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.MimeType;
Expand Down Expand Up @@ -75,12 +73,6 @@ public void canEncode() {

// SPR-15464
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();


assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "cbor", StandardCharsets.UTF_8))).isTrue();
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "cbor", StandardCharsets.ISO_8859_1))).isFalse();
}

@Test
Expand Down
Expand Up @@ -88,7 +88,7 @@ public void canDecode() {
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue();
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isFalse();
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue();
}

@Test // SPR-15866
Expand Down Expand Up @@ -235,6 +235,21 @@ public void decodeNonUtf8Encoding() {
null);
}

@Test
@SuppressWarnings("unchecked")
public void decodeNonUnicode() {
Flux<DataBuffer> input = Flux.concat(
stringBuffer("{\"føø\":\"bår\"}", StandardCharsets.ISO_8859_1)
);

testDecode(input, ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {
}),
step -> step.assertNext(o -> assertThat((Map<String, String>) o).containsEntry("føø", "bår"))
.verifyComplete(),
MediaType.parseMediaType("application/json; charset=iso-8859-1"),
null);
}

@Test
@SuppressWarnings("unchecked")
public void decodeMonoNonUtf8Encoding() {
Expand Down
Expand Up @@ -16,7 +16,6 @@

package org.springframework.http.codec.json;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

Expand All @@ -28,7 +27,6 @@
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.MimeType;
import org.springframework.web.testfixture.xml.Pojo;
Expand Down Expand Up @@ -65,12 +63,6 @@ public void canDecode() {

assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isFalse();

assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "x-jackson-smile", StandardCharsets.UTF_8))).isTrue();
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
new MediaType("application", "x-jackson-smile", StandardCharsets.ISO_8859_1))).isFalse();

}

@Override
Expand Down

0 comments on commit 525c8b1

Please sign in to comment.