diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Codec.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java similarity index 73% rename from spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Codec.java rename to spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java index aa1b3705bb..2348844c77 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Codec.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -16,34 +16,46 @@ package org.springframework.http.codec.json; +import java.lang.annotation.Annotation; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** - * Abstract base class for Jackson based decoder/encoder implementations. + * Base class providing support methods for Jackson 2 encoding and decoding. * * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 5.0 */ -public class AbstractJackson2Codec { +public abstract class Jackson2CodecSupport { /** - * Hint key to use with a {@link Class} value specifying the JSON View to use to serialize - * or deserialize an object. + * The key for the hint to specify a "JSON View" for encoding or decoding + * with the value expected to be a {@link Class}. * @see Jackson JSON Views */ - public static final String JSON_VIEW_HINT = AbstractJackson2Codec.class.getName() + ".jsonView"; + public static final String JSON_VIEW_HINT = Jackson2CodecSupport.class.getName() + ".jsonView"; + + private static final String JSON_VIEW_HINT_ERROR = + "@JsonView only supported for write hints with exactly 1 class argument: "; protected static final List JSON_MIME_TYPES = Arrays.asList( new MimeType("application", "json", StandardCharsets.UTF_8), @@ -54,10 +66,9 @@ public class AbstractJackson2Codec { /** - * Create a new Jackson codec for the given mapper. - * @param mapper the Jackson ObjectMapper to use + * Constructor with a Jackson {@link ObjectMapper} to use. */ - protected AbstractJackson2Codec(ObjectMapper mapper) { + protected Jackson2CodecSupport(ObjectMapper mapper) { Assert.notNull(mapper, "ObjectMapper must not be null"); this.mapper = mapper; } @@ -141,4 +152,22 @@ public class AbstractJackson2Codec { return ResolvableType.NONE; } + protected Map getHints(ResolvableType actualType) { + return getParameter(actualType) + .flatMap(parameter -> Optional.ofNullable(getAnnotation(parameter, JsonView.class)) + .map(annotation -> { + Class[] classes = annotation.value(); + Assert.isTrue(classes.length == 1, JSON_VIEW_HINT_ERROR + parameter); + return Collections.singletonMap(JSON_VIEW_HINT, classes[0]); + })) + .orElse(Collections.emptyMap()); + } + + protected Optional getParameter(ResolvableType type) { + return Optional.ofNullable (type.getSource() instanceof MethodParameter ? + (MethodParameter) type.getSource() : null); + } + + protected abstract A getAnnotation(MethodParameter parameter, Class 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 6ea5c90a39..3c7f8f1a51 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 @@ -17,11 +17,10 @@ package org.springframework.http.codec.json; import java.io.IOException; -import java.util.Collections; +import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; @@ -49,11 +48,11 @@ import org.springframework.util.MimeType; * @since 5.0 * @see Jackson2JsonEncoder */ -public class Jackson2JsonDecoder extends AbstractJackson2Codec implements ServerHttpDecoder { +public class Jackson2JsonDecoder extends Jackson2CodecSupport implements ServerHttpDecoder { - private final JsonObjectDecoder fluxObjectDecoder = new JsonObjectDecoder(true); + private final JsonObjectDecoder fluxDecoder = new JsonObjectDecoder(true); - private final JsonObjectDecoder monoObjectDecoder = new JsonObjectDecoder(false); + private final JsonObjectDecoder monoDecoder = new JsonObjectDecoder(false); public Jackson2JsonDecoder() { @@ -78,19 +77,17 @@ public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Server } @Override - public Flux decode(Publisher inputStream, ResolvableType elementType, + public Flux decode(Publisher input, ResolvableType elementType, MimeType mimeType, Map hints) { - JsonObjectDecoder objectDecoder = this.fluxObjectDecoder; - return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints); + return decodeInternal(this.fluxDecoder, input, elementType, mimeType, hints); } @Override - public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, + public Mono decodeToMono(Publisher input, ResolvableType elementType, MimeType mimeType, Map hints) { - JsonObjectDecoder objectDecoder = this.monoObjectDecoder; - return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints).singleOrEmpty(); + return decodeInternal(this.monoDecoder, input, elementType, mimeType, hints).singleOrEmpty(); } private Flux decodeInternal(JsonObjectDecoder objectDecoder, Publisher inputStream, @@ -99,20 +96,13 @@ public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Server Assert.notNull(inputStream, "'inputStream' must not be null"); Assert.notNull(elementType, "'elementType' must not be null"); - MethodParameter methodParam = (elementType.getSource() instanceof MethodParameter ? - (MethodParameter) elementType.getSource() : null); - Class contextClass = (methodParam != null ? methodParam.getContainingClass() : null); + Class contextClass = getParameter(elementType).map(MethodParameter::getContainingClass).orElse(null); JavaType javaType = getJavaType(elementType.getType(), contextClass); + Class jsonView = (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT); - ObjectReader reader; - Class jsonView = (Class)hints.get(AbstractJackson2Codec.JSON_VIEW_HINT); - - if (jsonView != null) { - reader = this.mapper.readerWithView(jsonView).forType(javaType); - } - else { - reader = this.mapper.readerFor(javaType); - } + ObjectReader reader = jsonView != null ? + this.mapper.readerWithView(jsonView).forType(javaType) : + this.mapper.readerFor(javaType); return objectDecoder.decode(inputStream, elementType, mimeType, hints) .map(dataBuffer -> { @@ -134,20 +124,12 @@ public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Server public Map getDecodeHints(ResolvableType actualType, ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) { - Object source = actualType.getSource(); - MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null); - if (parameter != null) { - JsonView annotation = parameter.getParameterAnnotation(JsonView.class); - if (annotation != null) { - Class[] classes = annotation.value(); - if (classes.length != 1) { - throw new IllegalArgumentException( - "@JsonView only supported for read hints with exactly 1 class argument: " + parameter); - } - return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]); - } - } - return Collections.emptyMap(); + 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 65cba1dad1..10741e82e4 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 @@ -18,11 +18,10 @@ package org.springframework.http.codec.json; import java.io.IOException; import java.io.OutputStream; -import java.util.HashMap; +import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; @@ -61,7 +60,7 @@ import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; * @since 5.0 * @see Jackson2JsonDecoder */ -public class Jackson2JsonEncoder extends AbstractJackson2Codec implements ServerHttpEncoder { +public class Jackson2JsonEncoder extends Jackson2CodecSupport implements ServerHttpEncoder { private final PrettyPrinter ssePrettyPrinter; @@ -112,30 +111,24 @@ public class Jackson2JsonEncoder extends AbstractJackson2Codec implements Server } private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, - ResolvableType type, Map hints) { + ResolvableType elementType, Map hints) { TypeFactory typeFactory = this.mapper.getTypeFactory(); - JavaType javaType = typeFactory.constructType(type.getType()); - if (type.isInstance(value)) { - javaType = getJavaType(type.getType(), null); + JavaType javaType = typeFactory.constructType(elementType.getType()); + if (elementType.isInstance(value)) { + javaType = getJavaType(elementType.getType(), null); } - ObjectWriter writer; - Class jsonView = (Class)hints.get(AbstractJackson2Codec.JSON_VIEW_HINT); - if (jsonView != null) { - writer = this.mapper.writerWithView(jsonView); - } - else { - writer = this.mapper.writer(); - } + Class jsonView = (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT); + ObjectWriter writer = jsonView != null ? this.mapper.writerWithView(jsonView): this.mapper.writer(); if (javaType != null && javaType.isContainerType()) { writer = writer.forType(javaType); } - Boolean sse = (Boolean)hints.get(ServerSentEventHttpMessageWriter.SSE_CONTENT_HINT); SerializationConfig config = writer.getConfig(); - if (Boolean.TRUE.equals(sse) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + Boolean sseHint = (Boolean) hints.get(ServerSentEventHttpMessageWriter.SSE_CONTENT_HINT); + if (Boolean.TRUE.equals(sseHint) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { writer = writer.with(this.ssePrettyPrinter); } @@ -158,21 +151,12 @@ public class Jackson2JsonEncoder extends AbstractJackson2Codec implements Server public Map getEncodeHints(ResolvableType actualType, ResolvableType elementType, MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { - Map hints = new HashMap<>(); - Object source = actualType.getSource(); - MethodParameter returnValue = (source instanceof MethodParameter ? (MethodParameter)source : null); - if (returnValue != null) { - JsonView annotation = returnValue.getMethodAnnotation(JsonView.class); - if (annotation != null) { - Class[] classes = annotation.value(); - if (classes.length != 1) { - throw new IllegalArgumentException( - "@JsonView only supported for write hints with exactly 1 class argument: " + returnValue); - } - hints.put(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]); - } - } - return hints; + return getHints(actualType); + } + + @Override + protected A getAnnotation(MethodParameter parameter, Class annotType) { + return parameter.getMethodAnnotation(annotType); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java index fea25f9528..d72be31f5a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java @@ -29,7 +29,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.codec.json.AbstractJackson2Codec; +import org.springframework.http.codec.json.Jackson2CodecSupport; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; @@ -207,7 +207,7 @@ public interface EntityResponse extends ServerResponse { Builder contentType(MediaType contentType); /** - * Add a serialization hint like {@link AbstractJackson2Codec#JSON_VIEW_HINT} to + * Add a serialization hint like {@link Jackson2CodecSupport#JSON_VIEW_HINT} to * customize how the body will be serialized. * @param key the hint key * @param value the hint value diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java index 373293a7e8..9909983d1c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java @@ -32,7 +32,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; -import org.springframework.http.codec.json.AbstractJackson2Codec; +import org.springframework.http.codec.json.Jackson2CodecSupport; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.server.WebSession; @@ -82,7 +82,7 @@ public interface ServerRequest { /** * Extract the body with the given {@code BodyExtractor} and hints. * @param extractor the {@code BodyExtractor} that reads from the request - * @param hints the map of hints like {@link AbstractJackson2Codec#JSON_VIEW_HINT} + * @param hints the map of hints like {@link Jackson2CodecSupport#JSON_VIEW_HINT} * to use to customize body extraction * @param the type of the body returned * @return the extracted body diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java index cf1764a2da..64cd5b1a5a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java @@ -31,7 +31,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.codec.json.AbstractJackson2Codec; +import org.springframework.http.codec.json.Jackson2CodecSupport; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.function.BodyInserter; @@ -326,7 +326,7 @@ public interface ServerResponse { BodyBuilder contentType(MediaType contentType); /** - * Add a serialization hint like {@link AbstractJackson2Codec#JSON_VIEW_HINT} + * Add a serialization hint like {@link Jackson2CodecSupport#JSON_VIEW_HINT} * to customize how the body will be serialized. * @param key the hint key * @param value the hint value diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java index 9a78d659c9..6e0c46064a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyExtractorsTests.java @@ -50,7 +50,7 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.util.MultiValueMap; import static org.junit.Assert.*; -import static org.springframework.http.codec.json.AbstractJackson2Codec.*; +import static org.springframework.http.codec.json.Jackson2CodecSupport.*; /** * @author Arjen Poutsma diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java index 8620dba03b..68a0b46bbc 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java @@ -61,7 +61,7 @@ import org.springframework.util.MultiValueMap; import static java.nio.charset.StandardCharsets.*; import static org.junit.Assert.*; -import static org.springframework.http.codec.json.AbstractJackson2Codec.*; +import static org.springframework.http.codec.json.Jackson2CodecSupport.*; /** * @author Arjen Poutsma