diff --git a/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java index e7812439b4..cd104d41cb 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java @@ -34,7 +34,11 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** - * Implementation of {@code HttpMessageReader} delegating to a {@link Decoder}. + * {@code HttpMessageReader} that wraps and delegates to a {@link Decoder}. + * + *

Also a {@code ServerHttpMessageReader} that pre-resolves decoding hints + * from the extra information available on the server side such as the request + * or controller method parameter annotations. * * @author Arjen Poutsma * @author Sebastien Deleuze @@ -105,7 +109,7 @@ public class DecoderHttpMessageReader implements ServerHttpMessageReader { ServerHttpRequest request, ServerHttpResponse response, Map hints) { Map allHints = new HashMap<>(4); - allHints.putAll(resolveReadHints(streamType, elementType, request, response)); + allHints.putAll(getReadHints(streamType, elementType, request, response)); allHints.putAll(hints); return read(elementType, request, allHints); @@ -116,19 +120,24 @@ public class DecoderHttpMessageReader implements ServerHttpMessageReader { ServerHttpRequest request, ServerHttpResponse response, Map hints) { Map allHints = new HashMap<>(4); - allHints.putAll(resolveReadHints(streamType, elementType, request, response)); + allHints.putAll(getReadHints(streamType, elementType, request, response)); allHints.putAll(hints); return readMono(elementType, request, allHints); } /** - * Resolve hints to pass to the decoder, e.g. by checking for annotations - * on a controller method parameter or checking the server request. + * Get additional hints for decoding for example based on the server request + * or annotations from controller method parameters. By default, delegate to + * the decoder if it is an instance of {@link ServerHttpDecoder}. */ - protected Map resolveReadHints(ResolvableType streamType, + protected Map getReadHints(ResolvableType streamType, ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) { + if (this.decoder instanceof ServerHttpDecoder) { + ServerHttpDecoder httpDecoder = (ServerHttpDecoder) this.decoder; + return httpDecoder.getDecodeHints(streamType, elementType, request, response); + } return Collections.emptyMap(); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java index 3ae2f9ddac..9dbfe37f8f 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java @@ -39,7 +39,11 @@ import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_H import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT; /** - * Implementation of {@code HttpMessageWriter} delegating to an {@link Encoder}. + * {@code HttpMessageWriter} that wraps and delegates to a {@link Encoder}. + * + *

Also a {@code ServerHttpMessageWriter} that pre-resolves encoding hints + * from the extra information available on the server side such as the request + * or controller method annotations. * * @author Arjen Poutsma * @author Sebastien Deleuze @@ -132,19 +136,24 @@ public class EncoderHttpMessageWriter implements ServerHttpMessageWriter { ServerHttpResponse response, Map hints) { Map allHints = new HashMap<>(); - allHints.putAll(resolveWriteHints(streamType, elementType, mediaType, request, response)); + allHints.putAll(getWriteHints(streamType, elementType, mediaType, request, response)); allHints.putAll(hints); return write(inputStream, elementType, mediaType, response, allHints); } /** - * Resolve hints to pass to the encoder, e.g. by checking for annotations - * on a controller method parameter or checking the server request. + * Get additional hints for encoding for example based on the server request + * or annotations from controller method parameters. By default, delegate to + * the encoder if it is an instance of {@link ServerHttpEncoder}. */ - protected Map resolveWriteHints(ResolvableType streamType, ResolvableType elementType, + protected Map getWriteHints(ResolvableType streamType, ResolvableType elementType, MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { + if (this.encoder instanceof ServerHttpEncoder) { + ServerHttpEncoder httpEncoder = (ServerHttpEncoder) this.encoder; + return httpEncoder.getEncodeHints(streamType, elementType, mediaType, request, response); + } return Collections.emptyMap(); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java deleted file mode 100644 index 50da50349a..0000000000 --- a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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; - -import java.util.Collections; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonView; - -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; -import org.springframework.http.codec.json.AbstractJackson2Codec; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; - -/** - * {@link ServerHttpMessageReader} that resolves those annotation or request based Jackson 2 hints: - *

- * - * @author Sebastien Deleuze - * @author Rossen Stoyanchev - * @since 5.0 - * @see com.fasterxml.jackson.annotation.JsonView - */ -public class Jackson2ServerHttpMessageReader extends DecoderHttpMessageReader { - - - public Jackson2ServerHttpMessageReader(Decoder decoder) { - super(decoder); - } - - - @Override - protected Map resolveReadHints(ResolvableType streamType, - ResolvableType elementType, ServerHttpRequest request, ServerHttpResponse response) { - - Object source = streamType.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(); - } - -} diff --git a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java index c7eedce36c..e06730195e 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java @@ -20,22 +20,20 @@ package org.springframework.http.codec; import java.util.HashMap; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonView; import org.reactivestreams.Publisher; -import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT; -import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT; import reactor.core.publisher.Mono; -import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.AbstractEncoder; import org.springframework.core.codec.Encoder; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; -import org.springframework.http.codec.json.AbstractJackson2Codec; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import static org.springframework.core.codec.AbstractEncoder.FLUSHING_STRATEGY_HINT; +import static org.springframework.core.codec.AbstractEncoder.FlushingStrategy.AFTER_EACH_ELEMENT; + /** * Jackson {@link ServerHttpMessageWriter} that resolves {@code @JsonView} annotated handler * method and deals with {@link AbstractEncoder#FLUSHING_STRATEGY_HINT}. @@ -76,25 +74,4 @@ public class Jackson2ServerHttpMessageWriter extends EncoderHttpMessageWriter resolveWriteHints(ResolvableType streamType, ResolvableType elementType, - MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { - - Map hints = new HashMap<>(); - Object source = streamType.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; - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java new file mode 100644 index 0000000000..fc256c9bd9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpDecoder.java @@ -0,0 +1,51 @@ +/* + * 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; + +import java.util.Map; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + +/** + * {@code Decoder} extension for server-side decoding of the HTTP request body. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public interface ServerHttpDecoder extends Decoder { + + + /** + * Get decoding hints based on the server request or annotations on the + * target controller method parameter. + * + * @param actualType the actual target type to decode to, possibly a reactive + * wrapper and sourced from {@link org.springframework.core.MethodParameter}, + * i.e. providing access to method parameter annotations. + * @param elementType the element type within {@code Flux/Mono} that we're + * trying to decode to. + * @param request the current request + * @param response the current response + * @return a Map with hints, possibly empty + */ + Map getDecodeHints(ResolvableType actualType, ResolvableType elementType, + ServerHttpRequest request, ServerHttpResponse response); + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java new file mode 100644 index 0000000000..20bc06583f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpEncoder.java @@ -0,0 +1,53 @@ +/* + * 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; + +import java.util.Map; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + + +/** + * {@code Encoder} extension for server-side encoding of the HTTP response body. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public interface ServerHttpEncoder extends Encoder { + + + /** + * Get decoding hints based on the server request or annotations on the + * target controller method parameter. + * + * @param actualType the actual source type to encode, possibly a reactive + * wrapper and sourced from {@link org.springframework.core.MethodParameter}, + * i.e. providing access to method annotations. + * @param elementType the element type within {@code Flux/Mono} that we're + * trying to encode. + * @param request the current request + * @param response the current response + * @return a Map with hints, possibly empty + */ + Map getEncodeHints(ResolvableType actualType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response); + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java index a7ca7be6fd..7f2032b137 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java @@ -35,6 +35,8 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.MimeTypeUtils; @@ -47,7 +49,7 @@ import org.springframework.util.MimeTypeUtils; * @author Arjen Poutsma * @since 5.0 */ -public class ServerSentEventHttpMessageWriter implements HttpMessageWriter { +public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter { /** * Server-Sent Events hint key expecting a {@link Boolean} value which when set to true @@ -161,4 +163,23 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter write(Publisher inputStream, ResolvableType streamType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response, + Map hints) { + + Map allHints = this.dataEncoders.stream() + .filter(encoder -> encoder instanceof ServerHttpEncoder) + .map(encoder -> (ServerHttpEncoder) encoder) + .map(encoder -> encoder.getEncodeHints(streamType, elementType, mediaType, request, response)) + .reduce(new HashMap<>(), (t, u) -> { + t.putAll(u); + return t; + }); + + allHints.putAll(hints); + + return write(inputStream, elementType, mediaType, response, allHints); + } + } 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 bfb75adcda..6ea5c90a39 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,9 +17,11 @@ package org.springframework.http.codec.json; import java.io.IOException; +import java.util.Collections; 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; @@ -30,10 +32,12 @@ 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.Decoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.codec.ServerHttpDecoder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -45,7 +49,7 @@ import org.springframework.util.MimeType; * @since 5.0 * @see Jackson2JsonEncoder */ -public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Decoder { +public class Jackson2JsonDecoder extends AbstractJackson2Codec implements ServerHttpDecoder { private final JsonObjectDecoder fluxObjectDecoder = new JsonObjectDecoder(true); @@ -123,4 +127,27 @@ public class Jackson2JsonDecoder extends AbstractJackson2Codec implements Decode }); } + + // ServerHttpDecoder... + + @Override + 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(); + } + } 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 2acede40f1..65cba1dad1 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,9 +18,11 @@ package org.springframework.http.codec.json; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; 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; @@ -31,20 +33,25 @@ import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.type.TypeFactory; import org.reactivestreams.Publisher; -import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; 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.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerHttpEncoder; import org.springframework.http.codec.ServerSentEventHttpMessageWriter; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.MimeType; +import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; + /** * Encode from an {@code Object} stream to a byte stream of JSON objects, * using Jackson 2.6+. @@ -54,7 +61,7 @@ import org.springframework.util.MimeType; * @since 5.0 * @see Jackson2JsonDecoder */ -public class Jackson2JsonEncoder extends AbstractJackson2Codec implements Encoder { +public class Jackson2JsonEncoder extends AbstractJackson2Codec implements ServerHttpEncoder { private final PrettyPrinter ssePrettyPrinter; @@ -144,4 +151,28 @@ public class Jackson2JsonEncoder extends AbstractJackson2Codec implements Encode return buffer; } + + // ServerHttpEncoder... + + @Override + 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; + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index 5d89ea0e26..042d27271d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -50,7 +50,6 @@ import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; -import org.springframework.http.codec.Jackson2ServerHttpMessageReader; import org.springframework.http.codec.Jackson2ServerHttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ServerSentEventHttpMessageWriter; @@ -339,8 +338,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { readers.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); } if (jackson2Present) { - readers.add(new Jackson2ServerHttpMessageReader( - new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()))); + readers.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); } }