From f46520e6e8e70e59ad46bc31debdfd155cf943bc Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 12 Jul 2017 17:44:07 +0200 Subject: [PATCH] Add Jackson Smile support to WebFlux This binary format more efficient than JSON should be useful for server to server communication, for example in micro-services use cases. Issue: SPR-15424 --- build.gradle | 1 + .../http/codec/AbstractCodecConfigurer.java | 36 ++-- .../http/codec/ClientCodecConfigurer.java | 2 +- .../http/codec/CodecConfigurer.java | 4 +- .../codec/DefaultClientCodecConfigurer.java | 2 +- .../codec/DefaultServerCodecConfigurer.java | 2 +- .../http/codec/ServerCodecConfigurer.java | 2 +- .../codec/json/AbstractJackson2Decoder.java | 149 +++++++++++++++ .../codec/json/AbstractJackson2Encoder.java | 170 ++++++++++++++++++ .../http/codec/json/Jackson2JsonDecoder.java | 115 +----------- .../http/codec/json/Jackson2JsonEncoder.java | 138 +------------- .../http/codec/json/Jackson2SmileDecoder.java | 56 ++++++ .../http/codec/json/Jackson2SmileEncoder.java | 56 ++++++ .../codec/ClientCodecConfigurerTests.java | 10 +- .../http/codec/CodecConfigurerTests.java | 18 +- .../codec/ServerCodecConfigurerTests.java | 10 +- .../codec/json/Jackson2SmileDecoderTests.java | 118 ++++++++++++ .../codec/json/Jackson2SmileEncoderTests.java | 126 +++++++++++++ .../DelegatingWebFluxConfigurationTests.java | 2 +- .../WebFluxConfigurationSupportTests.java | 9 +- 20 files changed, 749 insertions(+), 277 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java diff --git a/build.gradle b/build.gradle index be79ef6913..a8f92f03bb 100644 --- a/build.gradle +++ b/build.gradle @@ -952,6 +952,7 @@ project("spring-webflux") { optional "javax.servlet:javax.servlet-api:${servletVersion}" optional("javax.xml.bind:jaxb-api:${jaxbVersion}") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile:${jackson2Version}") optional("org.freemarker:freemarker:${freemarkerVersion}") optional("org.apache.httpcomponents:httpclient:${httpclientVersion}") { exclude group: "commons-logging", module: "commons-logging" diff --git a/spring-web/src/main/java/org/springframework/http/codec/AbstractCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/AbstractCodecConfigurer.java index ee52ca2e00..aa0e6e5fbb 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/AbstractCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/AbstractCodecConfigurer.java @@ -34,6 +34,8 @@ import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.Jackson2SmileDecoder; +import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.lang.Nullable; @@ -54,6 +56,10 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer { ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AbstractCodecConfigurer.class.getClassLoader()); + private static final boolean jackson2SmilePresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", + AbstractCodecConfigurer.class.getClassLoader()); + protected static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", AbstractCodecConfigurer.class.getClassLoader()); @@ -119,10 +125,10 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer { private boolean registerDefaults = true; @Nullable - private Jackson2JsonDecoder jackson2Decoder; + private Jackson2JsonDecoder jackson2JsonDecoder; @Nullable - private Jackson2JsonEncoder jackson2Encoder; + private Jackson2JsonEncoder jackson2JsonEncoder; @Nullable private DefaultCustomCodecs customCodecs; @@ -148,21 +154,21 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer { } @Override - public void jackson2Decoder(Jackson2JsonDecoder decoder) { - this.jackson2Decoder = decoder; + public void jackson2JsonDecoder(Jackson2JsonDecoder decoder) { + this.jackson2JsonDecoder = decoder; } - protected Jackson2JsonDecoder jackson2Decoder() { - return (this.jackson2Decoder != null ? this.jackson2Decoder : new Jackson2JsonDecoder()); + protected Jackson2JsonDecoder jackson2JsonDecoder() { + return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder()); } @Override - public void jackson2Encoder(Jackson2JsonEncoder encoder) { - this.jackson2Encoder = encoder; + public void jackson2JsonEncoder(Jackson2JsonEncoder encoder) { + this.jackson2JsonEncoder = encoder; } - protected Jackson2JsonEncoder jackson2Encoder() { - return (this.jackson2Encoder != null ? this.jackson2Encoder : new Jackson2JsonEncoder()); + protected Jackson2JsonEncoder jackson2JsonEncoder() { + return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder()); } // Readers... @@ -191,7 +197,10 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer { result.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); } if (jackson2Present) { - result.add(new DecoderHttpMessageReader<>(jackson2Decoder())); + result.add(new DecoderHttpMessageReader<>(jackson2JsonDecoder())); + } + if (jackson2SmilePresent) { + result.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder())); } return result; } @@ -229,7 +238,10 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer { result.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); } if (jackson2Present) { - result.add(new EncoderHttpMessageWriter<>(jackson2Encoder())); + result.add(new EncoderHttpMessageWriter<>(jackson2JsonEncoder())); + } + if (jackson2SmilePresent) { + result.add(new EncoderHttpMessageWriter<>(new Jackson2SmileEncoder())); } return result; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java index 375d0bccd6..84923fdc27 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java @@ -64,7 +64,7 @@ public interface ClientCodecConfigurer extends CodecConfigurer { /** * Configure the {@code Decoder} to use for Server-Sent Events. *

By default if this is not set, and Jackson is available, the - * {@link #jackson2Decoder} override is used instead. Use this property + * {@link #jackson2JsonDecoder} override is used instead. Use this property * if you want to further customize the SSE decoder. * @param decoder the decoder to use */ diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index d7624adb49..ae8afecbc6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -74,13 +74,13 @@ public interface CodecConfigurer { * Override the default Jackson JSON {@code Decoder}. * @param decoder the decoder instance to use */ - void jackson2Decoder(Jackson2JsonDecoder decoder); + void jackson2JsonDecoder(Jackson2JsonDecoder decoder); /** * Override the default Jackson JSON {@code Encoder}. * @param encoder the encoder instance to use */ - void jackson2Encoder(Jackson2JsonEncoder encoder); + void jackson2JsonEncoder(Jackson2JsonEncoder encoder); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java index b08a1f4c69..8535fb9b63 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java @@ -83,7 +83,7 @@ class DefaultClientCodecConfigurer extends AbstractCodecConfigurer implements Cl if (this.sseDecoder != null) { return this.sseDecoder; } - return (jackson2Present ? jackson2Decoder() : null); + return (jackson2Present ? jackson2JsonDecoder() : null); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/codec/DefaultServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/DefaultServerCodecConfigurer.java index 295c36b40c..18a6295888 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/DefaultServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/DefaultServerCodecConfigurer.java @@ -95,7 +95,7 @@ class DefaultServerCodecConfigurer extends AbstractCodecConfigurer implements Se if (this.sseEncoder != null) { return this.sseEncoder; } - return jackson2Present ? jackson2Encoder() : null; + return jackson2Present ? jackson2JsonEncoder() : null; } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java index 3c89a1838f..a840f8840b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -55,7 +55,7 @@ public interface ServerCodecConfigurer extends CodecConfigurer { /** * Configure the {@code Encoder} to use for Server-Sent Events. *

By default if this is not set, and Jackson is available, the - * {@link #jackson2Encoder} override is used instead. Use this property + * {@link #jackson2JsonEncoder} override is used instead. Use this property * if you want to further customize the SSE encoder. */ void serverSentEventEncoder(Encoder encoder); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java new file mode 100644 index 0000000000..71162eb49b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.http.codec.json; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.annotation.Annotation; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.util.TokenBuffer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.HttpMessageDecoder; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Base class providing support methods for Jackson 2.9 decoding. + * + * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 5.0 + */ +public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder { + + /** + * Constructor with a Jackson {@link ObjectMapper} to use. + */ + protected AbstractJackson2Decoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + } + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + JavaType javaType = objectMapper().getTypeFactory().constructType(elementType.getType()); + // Skip String: CharSequenceDecoder + "*/*" comes after + return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) && + objectMapper().canDeserialize(javaType) && supportsMimeType(mimeType)); + } + + @Override + public Flux decode(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux tokens = tokenize(input, true); + return decodeInternal(tokens, elementType, mimeType, hints); + } + + @Override + public Mono decodeToMono(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux tokens = tokenize(input, false); + return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); + } + + private Flux tokenize(Publisher input, boolean tokenizeArrayElements) { + try { + JsonFactory factory = objectMapper().getFactory(); + JsonParser nonBlockingParser = factory.createNonBlockingByteArrayParser(); + Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(nonBlockingParser, + tokenizeArrayElements); + return Flux.from(input) + .flatMap(tokenizer) + .doFinally(t -> tokenizer.endOfInput()); + } + catch (IOException ex) { + return Flux.error(new UncheckedIOException(ex)); + } + + } + + private Flux decodeInternal(Flux tokens, + ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + Assert.notNull(tokens, "'tokens' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + + MethodParameter param = getParameter(elementType); + Class contextClass = (param != null ? param.getContainingClass() : null); + JavaType javaType = getJavaType(elementType.getType(), contextClass); + Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); + + ObjectReader reader = (jsonView != null ? + objectMapper().readerWithView(jsonView).forType(javaType) : + objectMapper().readerFor(javaType)); + + return tokens.map(tokenBuffer -> { + try { + return reader.readValue(tokenBuffer.asParser()); + } + catch (InvalidDefinitionException ex) { + throw new CodecException("Type definition error: " + ex.getType(), ex); + } + catch (JsonProcessingException ex) { + throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex); + } + catch (IOException ex) { + throw new DecodingException("I/O error while parsing input stream", ex); + } + }); + } + + + // HttpMessageDecoder... + + @Override + public Map getDecodeHints(ResolvableType actualType, ResolvableType elementType, + ServerHttpRequest request, ServerHttpResponse response) { + + return getHints(actualType); + } + + @Override + protected A getAnnotation(MethodParameter parameter, Class annotType) { + return parameter.getParameterAnnotation(annotType); + } +} 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 new file mode 100644 index 0000000000..d8dcb5c397 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.http.codec.json; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.EncodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageEncoder; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Base class providing support methods for Jackson 2.9 encoding. + * + * @author Sebastien Deleuze + * @author Arjen Poutsma + * @since 5.0 + */ +public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport implements HttpMessageEncoder { + + protected final List streamingMediaTypes = new ArrayList<>(1); + + /** + * Constructor with a Jackson {@link ObjectMapper} to use. + */ + protected AbstractJackson2Encoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + } + + /** + * Configure "streaming" media types for which flushing should be performed + * automatically vs at the end of the stream. + *

By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}. + * @param mediaTypes one or more media types to add to the list + * @see HttpMessageEncoder#getStreamingMediaTypes() + */ + public void setStreamingMediaTypes(List mediaTypes) { + this.streamingMediaTypes.clear(); + this.streamingMediaTypes.addAll(mediaTypes); + } + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + Class clazz = elementType.resolve(Object.class); + return (Object.class == clazz) || + !String.class.isAssignableFrom(elementType.resolve(clazz)) && + objectMapper().canSerialize(clazz) && supportsMimeType(mimeType); + } + + @Override + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { + + Assert.notNull(inputStream, "'inputStream' must not be null"); + Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + + if (inputStream instanceof Mono) { + return Flux.from(inputStream).map(value -> + encodeValue(value, mimeType, bufferFactory, elementType, hints)); + } + else if (this.streamingMediaTypes.stream().anyMatch(streamingMediaType -> streamingMediaType.isCompatibleWith(mimeType))) { + return Flux.from(inputStream).map(value -> { + DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints); + buffer.write(new byte[]{'\n'}); + return buffer; + }); + } + else { + ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); + return Flux.from(inputStream).collectList().map(list -> + encodeValue(list, mimeType, bufferFactory, listType, hints)).flux(); + } + } + + private DataBuffer encodeValue(Object value, @Nullable MimeType mimeType, DataBufferFactory bufferFactory, + ResolvableType elementType, @Nullable Map hints) { + + JavaType javaType = getJavaType(elementType.getType(), null); + Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); + ObjectWriter writer = (jsonView != null ? + objectMapper().writerWithView(jsonView) : objectMapper().writer()); + + if (javaType.isContainerType()) { + writer = writer.forType(javaType); + } + + writer = customizeWriter(writer, mimeType, elementType, hints); + + DataBuffer buffer = bufferFactory.allocateBuffer(); + OutputStream outputStream = buffer.asOutputStream(); + try { + writer.writeValue(outputStream, value); + } + catch (InvalidDefinitionException ex) { + throw new CodecException("Type definition error: " + ex.getType(), ex); + } + catch (JsonProcessingException ex) { + throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); + } + catch (IOException ex) { + throw new IllegalStateException("Unexpected I/O error while writing to data buffer", ex); + } + + return buffer; + } + + protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType, + ResolvableType elementType, @Nullable Map hints) { + return writer; + } + + + // HttpMessageEncoder... + + @Override + public List getStreamingMediaTypes() { + return Collections.unmodifiableList(this.streamingMediaTypes); + } + + @Override + public Map getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType, + @Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { + + return (actualType != null ? getHints(actualType) : Collections.emptyMap()); + } + + @Override + protected A getAnnotation(MethodParameter parameter, Class annotType) { + return parameter.getMethodAnnotation(annotType); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java index 2bc2e89b6d..b7ea22ba83 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java @@ -16,35 +16,10 @@ package org.springframework.http.codec.json; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.annotation.Annotation; import java.util.List; -import java.util.Map; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; -import com.fasterxml.jackson.databind.util.TokenBuffer; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.DecodingException; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.codec.HttpMessageDecoder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.MimeType; /** @@ -55,7 +30,7 @@ import org.springframework.util.MimeType; * @since 5.0 * @see Jackson2JsonEncoder */ -public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpMessageDecoder { +public class Jackson2JsonDecoder extends AbstractJackson2Decoder { public Jackson2JsonDecoder() { super(Jackson2ObjectMapperBuilder.json().build()); @@ -65,96 +40,8 @@ public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpMes super(mapper, mimeTypes); } - @Override - public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { - JavaType javaType = objectMapper().getTypeFactory().constructType(elementType.getType()); - // Skip String: CharSequenceDecoder + "*/*" comes after - return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) && - objectMapper().canDeserialize(javaType) && supportsMimeType(mimeType)); - } - @Override public List getDecodableMimeTypes() { return JSON_MIME_TYPES; } - - @Override - public Flux decode(Publisher input, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - Flux tokens = tokenize(input, true); - return decodeInternal(tokens, elementType, mimeType, hints); - } - - @Override - public Mono decodeToMono(Publisher input, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - Flux tokens = tokenize(input, false); - return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); - } - - private Flux tokenize(Publisher input, boolean tokenizeArrayElements) { - try { - JsonFactory factory = objectMapper().getFactory(); - JsonParser nonBlockingParser = factory.createNonBlockingByteArrayParser(); - Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(nonBlockingParser, - tokenizeArrayElements); - return Flux.from(input) - .flatMap(tokenizer) - .doFinally(t -> tokenizer.endOfInput()); - } - catch (IOException ex) { - return Flux.error(new UncheckedIOException(ex)); - } - - } - - private Flux decodeInternal(Flux tokens, - ResolvableType elementType, @Nullable MimeType mimeType, - @Nullable Map hints) { - - Assert.notNull(tokens, "'tokens' must not be null"); - Assert.notNull(elementType, "'elementType' must not be null"); - - MethodParameter param = getParameter(elementType); - Class contextClass = (param != null ? param.getContainingClass() : null); - JavaType javaType = getJavaType(elementType.getType(), contextClass); - Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); - - ObjectReader reader = (jsonView != null ? - objectMapper().readerWithView(jsonView).forType(javaType) : - objectMapper().readerFor(javaType)); - - return tokens.map(tokenBuffer -> { - try { - return reader.readValue(tokenBuffer.asParser()); - } - catch (InvalidDefinitionException ex) { - throw new CodecException("Type definition error: " + ex.getType(), ex); - } - catch (JsonProcessingException ex) { - throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex); - } - catch (IOException ex) { - throw new DecodingException("I/O error while parsing input stream", ex); - } - }); - } - - - // HttpMessageDecoder... - - @Override - public Map getDecodeHints(ResolvableType actualType, ResolvableType elementType, - ServerHttpRequest request, ServerHttpResponse response) { - - return getHints(actualType); - } - - @Override - protected A getAnnotation(MethodParameter parameter, Class annotType) { - return parameter.getParameterAnnotation(annotType); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java index 8ed922408c..2517730325 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java @@ -16,40 +16,20 @@ package org.springframework.http.codec.json; -import java.io.IOException; -import java.io.OutputStream; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.EncodingException; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.MediaType; -import org.springframework.http.codec.HttpMessageEncoder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.MimeType; /** @@ -61,13 +41,10 @@ import org.springframework.util.MimeType; * @since 5.0 * @see Jackson2JsonDecoder */ -public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMessageEncoder { - - private final List streamingMediaTypes = new ArrayList<>(1); - +public class Jackson2JsonEncoder extends AbstractJackson2Encoder { + private final PrettyPrinter ssePrettyPrinter; - - + public Jackson2JsonEncoder() { this(Jackson2ObjectMapperBuilder.json().build()); @@ -85,113 +62,16 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMes return printer; } - - /** - * Configure "streaming" media types for which flushing should be performed - * automatically vs at the end of the stream. - *

By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}. - * @param mediaTypes one or more media types to add to the list - * @see HttpMessageEncoder#getStreamingMediaTypes() - */ - public void setStreamingMediaTypes(List mediaTypes) { - this.streamingMediaTypes.clear(); - this.streamingMediaTypes.addAll(mediaTypes); - } - @Override - public List getEncodableMimeTypes() { - return JSON_MIME_TYPES; - } - - - @Override - public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { - Class clazz = elementType.resolve(Object.class); - return (Object.class == clazz) || - !String.class.isAssignableFrom(elementType.resolve(clazz)) && - objectMapper().canSerialize(clazz) && supportsMimeType(mimeType); - } - - @Override - public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, - ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - - Assert.notNull(inputStream, "'inputStream' must not be null"); - Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); - Assert.notNull(elementType, "'elementType' must not be null"); - - if (inputStream instanceof Mono) { - return Flux.from(inputStream).map(value -> - encodeValue(value, mimeType, bufferFactory, elementType, hints)); - } - else if (this.streamingMediaTypes.stream().anyMatch(streamingMediaType -> streamingMediaType.isCompatibleWith(mimeType))) { - return Flux.from(inputStream).map(value -> { - DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints); - buffer.write(new byte[]{'\n'}); - return buffer; - }); - } - else { - ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); - return Flux.from(inputStream).collectList().map(list -> - encodeValue(list, mimeType, bufferFactory, listType, hints)).flux(); - } - } - - private DataBuffer encodeValue(Object value, @Nullable MimeType mimeType, DataBufferFactory bufferFactory, + protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType, ResolvableType elementType, @Nullable Map hints) { - - JavaType javaType = getJavaType(elementType.getType(), null); - Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); - ObjectWriter writer = (jsonView != null ? - objectMapper().writerWithView(jsonView) : objectMapper().writer()); - - if (javaType.isContainerType()) { - writer = writer.forType(javaType); - } - - if (MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mimeType) && - writer.getConfig().isEnabled(SerializationFeature.INDENT_OUTPUT)) { - - writer = writer.with(this.ssePrettyPrinter); - } - - DataBuffer buffer = bufferFactory.allocateBuffer(); - OutputStream outputStream = buffer.asOutputStream(); - try { - writer.writeValue(outputStream, value); - } - catch (InvalidDefinitionException ex) { - throw new CodecException("Type definition error: " + ex.getType(), ex); - } - catch (JsonProcessingException ex) { - throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); - } - catch (IOException ex) { - throw new IllegalStateException("Unexpected I/O error while writing to data buffer", ex); - } - - return buffer; + + return (this.ssePrettyPrinter != null && MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mimeType) && + writer.getConfig().isEnabled(SerializationFeature.INDENT_OUTPUT) ? writer.with(this.ssePrettyPrinter) : writer); } - - // HttpMessageEncoder... - @Override - public List getStreamingMediaTypes() { - return Collections.unmodifiableList(this.streamingMediaTypes); - } - - @Override - public Map getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType, - @Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { - - return (actualType != null ? getHints(actualType) : Collections.emptyMap()); - } - - @Override - protected A getAnnotation(MethodParameter parameter, Class annotType) { - return parameter.getMethodAnnotation(annotType); + public List getEncodableMimeTypes() { + return JSON_MIME_TYPES; } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java new file mode 100644 index 0000000000..0e15943552 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.json; + +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Decode a byte stream into Smile and convert to Object's with Jackson 2.9. + * + * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 5.0 + * @see Jackson2JsonEncoder + */ +public class Jackson2SmileDecoder extends AbstractJackson2Decoder { + + private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile"); + + + public Jackson2SmileDecoder() { + this(Jackson2ObjectMapperBuilder.smile().build(), SMILE_MIME_TYPE); + } + + public Jackson2SmileDecoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass()); + } + + @Override + public List getDecodableMimeTypes() { + return Arrays.asList(SMILE_MIME_TYPE); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java new file mode 100644 index 0000000000..4614aa4364 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.json; + +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Encode from an {@code Object} stream to a byte stream of Smile objects using Jackson 2.9. + * + * @author Sebastien Deleuze + * @since 5.0 + * @see Jackson2SmileDecoder + */ +public class Jackson2SmileEncoder extends AbstractJackson2Encoder { + + private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile"); + + + public Jackson2SmileEncoder() { + this(Jackson2ObjectMapperBuilder.smile().build(), new MediaType("application", "x-jackson-smile")); + } + + public Jackson2SmileEncoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass()); + this.streamingMediaTypes.add(new MediaType("application", "stream+x-jackson-smile")); + } + + @Override + public List getEncodableMimeTypes() { + return Arrays.asList(SMILE_MIME_TYPE); + } +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java index 3bee998273..a1493cbd06 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java @@ -40,6 +40,8 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.Jackson2SmileDecoder; +import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; @@ -62,7 +64,7 @@ public class ClientCodecConfigurerTests { @Test public void defaultReaders() throws Exception { List> readers = this.configurer.getReaders(); - assertEquals(9, readers.size()); + assertEquals(10, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); @@ -70,6 +72,7 @@ public class ClientCodecConfigurerTests { assertStringDecoder(getNextDecoder(readers), true); assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass()); assertSseReader(readers); assertStringDecoder(getNextDecoder(readers), false); } @@ -77,7 +80,7 @@ public class ClientCodecConfigurerTests { @Test public void defaultWriters() throws Exception { List> writers = this.configurer.getWriters(); - assertEquals(10, writers.size()); + assertEquals(11, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); @@ -87,6 +90,7 @@ public class ClientCodecConfigurerTests { assertEquals(MultipartHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass()); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertStringEncoder(getNextEncoder(writers), false); } @@ -94,7 +98,7 @@ public class ClientCodecConfigurerTests { public void jackson2EncoderOverride() throws Exception { Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); - this.configurer.defaultCodecs().jackson2Decoder(decoder); + this.configurer.defaultCodecs().jackson2JsonDecoder(decoder); assertSame(decoder, this.configurer.getReaders().stream() .filter(reader -> ServerSentEventHttpMessageReader.class.equals(reader.getClass())) diff --git a/spring-web/src/test/java/org/springframework/http/codec/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/CodecConfigurerTests.java index 4c33e9b483..5afc47b407 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/CodecConfigurerTests.java @@ -35,6 +35,8 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.http.MediaType; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.Jackson2SmileDecoder; +import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.MimeTypeUtils; @@ -60,7 +62,7 @@ public class CodecConfigurerTests { @Test public void defaultReaders() throws Exception { List> readers = this.configurer.getReaders(); - assertEquals(8, readers.size()); + assertEquals(9, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); @@ -68,13 +70,14 @@ public class CodecConfigurerTests { assertStringDecoder(getNextDecoder(readers), true); assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass()); assertStringDecoder(getNextDecoder(readers), false); } @Test public void defaultWriters() throws Exception { List> writers = this.configurer.getWriters(); - assertEquals(8, writers.size()); + assertEquals(9, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); @@ -82,6 +85,7 @@ public class CodecConfigurerTests { assertStringEncoder(getNextEncoder(writers), true); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertStringEncoder(getNextEncoder(writers), false); } @@ -108,7 +112,7 @@ public class CodecConfigurerTests { List> readers = this.configurer.getReaders(); - assertEquals(12, readers.size()); + assertEquals(13, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); @@ -118,6 +122,7 @@ public class CodecConfigurerTests { assertSame(customReader1, readers.get(this.index.getAndIncrement())); assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass()); assertSame(customDecoder2, getNextDecoder(readers)); assertSame(customReader2, readers.get(this.index.getAndIncrement())); assertEquals(StringDecoder.class, getNextDecoder(readers).getClass()); @@ -146,7 +151,7 @@ public class CodecConfigurerTests { List> writers = this.configurer.getWriters(); - assertEquals(12, writers.size()); + assertEquals(13, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); @@ -156,6 +161,7 @@ public class CodecConfigurerTests { assertSame(customWriter1, writers.get(this.index.getAndIncrement())); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertSame(customEncoder2, getNextEncoder(writers)); assertSame(customWriter2, writers.get(this.index.getAndIncrement())); assertEquals(CharSequenceEncoder.class, getNextEncoder(writers).getClass()); @@ -229,7 +235,7 @@ public class CodecConfigurerTests { public void jackson2DecoderOverride() throws Exception { Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); - this.configurer.defaultCodecs().jackson2Decoder(decoder); + this.configurer.defaultCodecs().jackson2JsonDecoder(decoder); assertSame(decoder, this.configurer.getReaders().stream() .filter(writer -> writer instanceof DecoderHttpMessageReader) @@ -243,7 +249,7 @@ public class CodecConfigurerTests { public void jackson2EncoderOverride() throws Exception { Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); - this.configurer.defaultCodecs().jackson2Encoder(encoder); + this.configurer.defaultCodecs().jackson2JsonEncoder(encoder); assertSame(encoder, this.configurer.getWriters().stream() .filter(writer -> writer instanceof EncoderHttpMessageWriter) diff --git a/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java index 07c8c5f08f..6551d51643 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java @@ -41,6 +41,8 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.Jackson2SmileDecoder; +import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; @@ -64,7 +66,7 @@ public class ServerCodecConfigurerTests { @Test public void defaultReaders() throws Exception { List> readers = this.configurer.getReaders(); - assertEquals(11, readers.size()); + assertEquals(12, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); @@ -75,13 +77,14 @@ public class ServerCodecConfigurerTests { assertEquals(MultipartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass()); assertStringDecoder(getNextDecoder(readers), false); } @Test public void defaultWriters() throws Exception { List> writers = this.configurer.getWriters(); - assertEquals(9, writers.size()); + assertEquals(10, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); @@ -89,6 +92,7 @@ public class ServerCodecConfigurerTests { assertStringEncoder(getNextEncoder(writers), true); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertSseWriter(writers); assertStringEncoder(getNextEncoder(writers), false); } @@ -97,7 +101,7 @@ public class ServerCodecConfigurerTests { public void jackson2EncoderOverride() throws Exception { Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); - this.configurer.defaultCodecs().jackson2Encoder(encoder); + this.configurer.defaultCodecs().jackson2JsonEncoder(encoder); assertSame(encoder, this.configurer.getWriters().stream() .filter(writer -> ServerSentEventHttpMessageWriter.class.equals(writer.getClass())) diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java new file mode 100644 index 0000000000..1a20f91314 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.json; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.CodecException; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.Pojo; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.MimeType; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.http.MediaType.APPLICATION_JSON; + +/** + * Unit tests for {@link Jackson2SmileDecoder}. + * + * @author Sebastien Deleuze + */ +public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestCase { + + private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile"); + + private final Jackson2SmileDecoder decoder = new Jackson2SmileDecoder(); + + @Test + public void canDecode() { + assertTrue(decoder.canDecode(forClass(Pojo.class), SMILE_MIME_TYPE)); + assertTrue(decoder.canDecode(forClass(Pojo.class), null)); + + assertFalse(decoder.canDecode(forClass(String.class), null)); + assertFalse(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON)); + } + + @Test + public void decodePojo() throws Exception { + ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); + Pojo pojo = new Pojo("foo", "bar"); + byte[] serializedPojo = mapper.writer().writeValueAsBytes(pojo); + + Flux source = Flux.just(this.bufferFactory.wrap(serializedPojo)); + ResolvableType elementType = forClass(Pojo.class); + Flux flux = decoder.decode(source, elementType, null, emptyMap()); + + StepVerifier.create(flux) + .expectNext(pojo) + .verifyComplete(); + } + + @Test + public void decodePojoWithError() throws Exception { + Flux source = Flux.just(stringBuffer("123")); + ResolvableType elementType = forClass(Pojo.class); + Flux flux = decoder.decode(source, elementType, null, emptyMap()); + + StepVerifier.create(flux).verifyError(CodecException.class); + } + + @Test + public void decodeToList() throws Exception { + ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); + List list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")); + byte[] serializedList = mapper.writer().writeValueAsBytes(list); + Flux source = Flux.just(this.bufferFactory.wrap(serializedList)); + + ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class); + Mono mono = decoder.decodeToMono(source, elementType, null, emptyMap()); + + StepVerifier.create(mono) + .expectNext(list) + .expectComplete() + .verify(); + } + + @Test + public void decodeToFlux() throws Exception { + ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); + List list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")); + byte[] serializedList = mapper.writer().writeValueAsBytes(list); + Flux source = Flux.just(this.bufferFactory.wrap(serializedList)); + + ResolvableType elementType = forClass(Pojo.class); + Flux flux = decoder.decode(source, elementType, null, emptyMap()); + + StepVerifier.create(flux) + .expectNext(new Pojo("f1", "b1")) + .expectNext(new Pojo("f2", "b2")) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java new file mode 100644 index 0000000000..1533589c6f --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.json; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.MediaType; +import org.springframework.http.codec.Pojo; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.MimeType; + +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_XML; + +/** + * Unit tests for {@link Jackson2SmileEncoder}. + * + * @author Sebastien Deleuze + */ +public class Jackson2SmileEncoderTests extends AbstractDataBufferAllocatingTestCase { + + private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile"); + + private final Jackson2SmileEncoder encoder = new Jackson2SmileEncoder(); + + + @Test + public void canEncode() { + ResolvableType pojoType = ResolvableType.forClass(Pojo.class); + assertTrue(this.encoder.canEncode(pojoType, SMILE_MIME_TYPE)); + assertTrue(this.encoder.canEncode(pojoType, null)); + + // SPR-15464 + assertTrue(this.encoder.canEncode(ResolvableType.NONE, null)); + } + + @Test + public void canNotEncode() { + assertFalse(this.encoder.canEncode(ResolvableType.forClass(String.class), null)); + assertFalse(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), APPLICATION_XML)); + + ResolvableType sseType = ResolvableType.forClass(ServerSentEvent.class); + assertFalse(this.encoder.canEncode(sseType, SMILE_MIME_TYPE)); + } + + @Test + public void encode() throws Exception { + Flux source = Flux.just( + new Pojo("foo", "bar"), + new Pojo("foofoo", "barbar"), + new Pojo("foofoofoo", "barbarbar") + ); + ResolvableType type = ResolvableType.forClass(Pojo.class); + Flux output = this.encoder.encode(source, this.bufferFactory, type, null, emptyMap()); + + ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); + StepVerifier.create(output) + .consumeNextWith(dataBuffer -> readPojo(mapper, List.class, dataBuffer)) + .verifyComplete(); + } + + @Test + public void encodeAsStream() throws Exception { + Flux source = Flux.just( + new Pojo("foo", "bar"), + new Pojo("foofoo", "barbar"), + new Pojo("foofoofoo", "barbarbar") + ); + ResolvableType type = ResolvableType.forClass(Pojo.class); + MediaType mediaType = new MediaType("application", "stream+x-jackson-smile"); + Flux output = this.encoder.encode(source, this.bufferFactory, type, mediaType, emptyMap()); + + ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); + StepVerifier.create(output) + .consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer)) + .consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer)) + .consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer)) + .verifyComplete(); + } + + public T readPojo(ObjectMapper mapper, Class valueType, DataBuffer dataBuffer) { + try { + T value = mapper.reader().forType(valueType).readValue(DataBufferTestUtils.dumpBytes(dataBuffer)); + DataBufferUtils.release(dataBuffer); + return value; + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java index 8e5204f8eb..408a6da1da 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java @@ -97,7 +97,7 @@ public class DelegatingWebFluxConfigurationTests { verify(webFluxConfigurer).configureArgumentResolvers(any()); assertSame(formatterRegistry.getValue(), initializerConversionService); - assertEquals(11, codecsConfigurer.getValue().getReaders().size()); + assertEquals(12, codecsConfigurer.getValue().getReaders().size()); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index f700e7f1c1..ed08ee0744 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -138,7 +138,7 @@ public class WebFluxConfigurationSupportTests { assertNotNull(adapter); List> readers = adapter.getMessageCodecConfigurer().getReaders(); - assertEquals(11, readers.size()); + assertEquals(12, readers.size()); assertHasMessageReader(readers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageReader(readers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); @@ -147,6 +147,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageReader(readers, forClassWithGenerics(MultiValueMap.class, String.class, String.class), APPLICATION_FORM_URLENCODED); assertHasMessageReader(readers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageReader(readers, forClass(TestBean.class), APPLICATION_JSON); + assertHasMessageReader(readers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); assertHasMessageReader(readers, forClass(TestBean.class), null); WebBindingInitializer bindingInitializer = adapter.getWebBindingInitializer(); @@ -189,7 +190,7 @@ public class WebFluxConfigurationSupportTests { assertEquals(0, handler.getOrder()); List> writers = handler.getMessageWriters(); - assertEquals(9, writers.size()); + assertEquals(10, writers.size()); assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); @@ -197,6 +198,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageWriter(writers, forClass(Resource.class), IMAGE_PNG); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_JSON); + assertHasMessageWriter(writers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); assertHasMessageWriter(writers, forClass(TestBean.class), MediaType.parseMediaType("text/event-stream")); name = "webFluxContentTypeResolver"; @@ -215,7 +217,7 @@ public class WebFluxConfigurationSupportTests { assertEquals(100, handler.getOrder()); List> writers = handler.getMessageWriters(); - assertEquals(9, writers.size()); + assertEquals(10, writers.size()); assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); @@ -223,6 +225,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageWriter(writers, forClass(Resource.class), IMAGE_PNG); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_JSON); + assertHasMessageWriter(writers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); assertHasMessageWriter(writers, forClass(TestBean.class), null); name = "webFluxContentTypeResolver"; -- GitLab