From e74c59bf301ed18eb52fe79df03f1c3cef230a7e Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 16 Sep 2016 12:08:23 +0200 Subject: [PATCH] Introduce ServerHttpMessageWriter/Reader to resolve hints Issue: SPR-14693 --- ...AbstractMessageReaderArgumentResolver.java | 18 +++- .../AbstractMessageWriterResultHandler.java | 20 ++-- .../AbstractServerHttpMessageReader.java | 94 +++++++++++++++++++ .../AbstractServerHttpMessageWriter.java | 91 ++++++++++++++++++ .../http/codec/ServerHttpMessageReader.java | 48 ++++++++++ .../http/codec/ServerHttpMessageWriter.java | 48 ++++++++++ 6 files changed, 307 insertions(+), 12 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index 5f989412868..44e779dfbdb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.annotation.Annotation; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -32,6 +33,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.ServerHttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -115,8 +117,8 @@ public abstract class AbstractMessageReaderArgumentResolver { protected Mono readBody(MethodParameter bodyParameter, boolean isBodyRequired, ServerWebExchange exchange) { - Class bodyType = ResolvableType.forMethodParameter(bodyParameter).resolve(); - ReactiveAdapter adapter = getAdapterRegistry().getAdapterTo(bodyType); + ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter); + ReactiveAdapter adapter = getAdapterRegistry().getAdapterTo(bodyType.resolve()); ResolvableType elementType = ResolvableType.forMethodParameter(bodyParameter); if (adapter != null) { @@ -130,9 +132,15 @@ public abstract class AbstractMessageReaderArgumentResolver { } for (HttpMessageReader reader : getMessageReaders()) { - if (reader.canRead(elementType, mediaType, Collections.emptyMap())) { + + Map hints = (reader instanceof ServerHttpMessageReader ? + ((ServerHttpMessageReader)reader).resolveReadHints(bodyType, elementType, + mediaType, exchange.getRequest()) : Collections.emptyMap()); + + if (reader.canRead(elementType, mediaType, hints)) { + if (adapter != null && adapter.getDescriptor().isMultiValue()) { - Flux flux = reader.read(elementType, request, Collections.emptyMap()) + Flux flux = reader.read(elementType, request, hints) .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter))); if (checkRequired(adapter, isBodyRequired)) { flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter))); @@ -143,7 +151,7 @@ public abstract class AbstractMessageReaderArgumentResolver { return Mono.just(adapter.fromPublisher(flux)); } else { - Mono mono = reader.readMono(elementType, request, Collections.emptyMap()) + Mono mono = reader.readMono(elementType, request, hints) .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter))); if (checkRequired(adapter, isBodyRequired)) { mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter))); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index e4d6bfc9f77..f0e04a226ea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.reactivestreams.Publisher; @@ -28,6 +29,7 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ServerHttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -89,10 +91,10 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat @SuppressWarnings("unchecked") - protected Mono writeBody(Object body, MethodParameter bodyType, ServerWebExchange exchange) { + protected Mono writeBody(Object body, MethodParameter bodyParameter, ServerWebExchange exchange) { - Class bodyClass = bodyType.getParameterType(); - ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(bodyClass, body); + ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter); + ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(bodyType.resolve(), body); Publisher publisher; ResolvableType elementType; @@ -100,11 +102,11 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat publisher = adapter.toPublisher(body); elementType = adapter.getDescriptor().isNoValue() ? ResolvableType.forClass(Void.class) : - ResolvableType.forMethodParameter(bodyType).getGeneric(0); + bodyType.getGeneric(0); } else { publisher = Mono.justOrEmpty(body); - elementType = ResolvableType.forMethodParameter(bodyType); + elementType = bodyType; } if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) { @@ -121,10 +123,14 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat if (bestMediaType != null) { for (HttpMessageWriter messageWriter : getMessageWriters()) { - if (messageWriter.canWrite(elementType, bestMediaType, Collections.emptyMap())) { + Map hints = (messageWriter instanceof ServerHttpMessageWriter ? + ((ServerHttpMessageWriter)messageWriter).resolveWriteHints(bodyType, elementType, + bestMediaType, exchange.getRequest()) : Collections.emptyMap()); + if (messageWriter.canWrite(elementType, bestMediaType, hints)) { + ServerHttpResponse response = exchange.getResponse(); return messageWriter.write((Publisher) publisher, elementType, - bestMediaType, response, Collections.emptyMap()); + bestMediaType, response, hints); } } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java new file mode 100644 index 00000000000..ba573c10548 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageReader.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2016 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.HashMap; +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.server.reactive.ServerHttpRequest; + +/** + * {@link HttpMessageReader} wrapper to extend that implements {@link ServerHttpMessageReader} in order + * to allow providing hints. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public abstract class AbstractServerHttpMessageReader implements ServerHttpMessageReader { + + private HttpMessageReader reader; + + + public AbstractServerHttpMessageReader(HttpMessageReader reader) { + this.reader = reader; + } + + @Override + public boolean canRead(ResolvableType elementType, MediaType mediaType, Map hints) { + return this.reader.canRead(elementType, mediaType, hints); + } + + @Override + public Flux read(ResolvableType elementType, ReactiveHttpInputMessage inputMessage, Map hints) { + return this.reader.read(elementType, inputMessage, hints); + } + + @Override + public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage inputMessage, Map hints) { + return this.reader.readMono(elementType, inputMessage, hints); + } + + @Override + public List getReadableMediaTypes() { + return this.reader.getReadableMediaTypes(); + } + + @Override + public final Map resolveReadHints(ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + + Map hints = new HashMap<>(); + if (this.reader instanceof ServerHttpMessageReader) { + hints.putAll(((ServerHttpMessageReader)this.reader).resolveReadHints(streamType, elementType, mediaType, request)); + } + hints.putAll(resolveReadHintsInternal(streamType, elementType, mediaType, request)); + return hints; + } + + /** + * Abstract method that returns hints which can be used to customize how the body should be read. + * Invoked from {@link #resolveReadHints}. + * @param streamType the original type used in the method parameter. For annotation + * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. + * @param elementType the stream element type to return + * @param mediaType the media type to read, can be {@code null} if not specified. + * Typically the value of a {@code Content-Type} header. + * @param request the current HTTP request + * @return Additional information about how to read the body + */ + protected abstract Map resolveReadHintsInternal(ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request); + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java new file mode 100644 index 00000000000..f386c61cb71 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/AbstractServerHttpMessageWriter.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 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.HashMap; +import java.util.List; +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.server.reactive.ServerHttpRequest; + +/** + * {@link HttpMessageWriter} wrapper to extend that implements {@link ServerHttpMessageWriter} in order + * to allow providing hints. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public abstract class AbstractServerHttpMessageWriter implements ServerHttpMessageWriter { + + private HttpMessageWriter writer; + + + public AbstractServerHttpMessageWriter(HttpMessageWriter writer) { + this.writer = writer; + } + + @Override + public boolean canWrite(ResolvableType elementType, MediaType mediaType, Map hints) { + return this.writer.canWrite(elementType, mediaType, hints); + } + + @Override + public Mono write(Publisher inputStream, ResolvableType elementType, + MediaType mediaType, ReactiveHttpOutputMessage outputMessage, Map hints) { + + return this.writer.write(inputStream, elementType, mediaType, outputMessage, hints); + } + + @Override + public List getWritableMediaTypes() { + return this.writer.getWritableMediaTypes(); + } + + @Override + public final Map resolveWriteHints(ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + + Map hints = new HashMap<>(); + if (this.writer instanceof ServerHttpMessageWriter) { + hints.putAll(((ServerHttpMessageWriter)this.writer).resolveWriteHints(streamType, elementType, mediaType, request)); + } + hints.putAll(resolveWriteHintsInternal(streamType, elementType, mediaType, request)); + return hints; + } + + /** + * Abstract method that returns hints which can be used to customize how the body should be written. + * Invoked from {@link #resolveWriteHints}. + * @param streamType the original type used for the method return value. For annotation + * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. + * @param elementType the stream element type to process + * @param mediaType the content type to use when writing. May be {@code null} to + * indicate that the default content type of the converter must be used. + * @param request the current HTTP request + * @return Additional information about how to write the body + */ + protected abstract Map resolveWriteHintsInternal(ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request); + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java new file mode 100644 index 00000000000..6f085cf1c03 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageReader.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; + +/** + * Server and annotation based controller specific {@link HttpMessageReader} that allows to + * resolve hints using annotations or request based information. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public interface ServerHttpMessageReader extends HttpMessageReader { + + /** + * Return hints that can be used to customize how the body should be read + * @param streamType the original type used in the method parameter. For annotation + * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. + * @param elementType the stream element type to return + * @param mediaType the media type to read, can be {@code null} if not specified. + * Typically the value of a {@code Content-Type} header. + * @param request the current HTTP request + * @return Additional information about how to read the body + */ + Map resolveReadHints(ResolvableType streamType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request); + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java new file mode 100644 index 00000000000..f72a5787115 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerHttpMessageWriter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; + +/** + * Server oriented {@link HttpMessageWriter} that allows to resolve hints using annotations or + * request based information. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public interface ServerHttpMessageWriter extends HttpMessageWriter { + + /** + * Return hints that can be used to customize how the body should be written + * @param streamType the original type used for the method return value. For annotation + * based controllers, the {@link MethodParameter} is available via {@link ResolvableType#getSource()}. + * @param elementType the stream element type to process + * @param mediaType the content type to use when writing. May be {@code null} to + * indicate that the default content type of the converter must be used. + * @param request the current HTTP request + * @return Additional information about how to write the body + */ + Map resolveWriteHints(ResolvableType streamType, ResolvableType elementType, + MediaType mediaType, ServerHttpRequest request); + +}