diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java new file mode 100644 index 00000000000..fd0b0fb4b7f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java @@ -0,0 +1,148 @@ +/* + * 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.web.reactive.result; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.springframework.core.Ordered; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Base class for {@link org.springframework.web.reactive.HandlerResultHandler + * HandlerResultHandler} implementations that perform content negotiation. + * + * @author Rossen Stoyanchev + */ +public abstract class ContentNegotiatingResultHandlerSupport implements Ordered { + + private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application"); + + + private final ConversionService conversionService; + + private final RequestedContentTypeResolver contentTypeResolver; + + private int order = LOWEST_PRECEDENCE; + + + protected ContentNegotiatingResultHandlerSupport(ConversionService conversionService, + RequestedContentTypeResolver contentTypeResolver) { + + Assert.notNull(conversionService, "'conversionService' is required."); + Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); + this.conversionService = conversionService; + this.contentTypeResolver = contentTypeResolver; + } + + + /** + * Return the configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + + /** + * Return the configured {@link RequestedContentTypeResolver}. + */ + public RequestedContentTypeResolver getContentTypeResolver() { + return this.contentTypeResolver; + } + + /** + * Set the order for this result handler relative to others. + *

By default set to {@link Ordered#LOWEST_PRECEDENCE}, however see + * Javadoc of sub-classes which may change this default. + * @param order the order + */ + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + /** + * Select the best media type for the current request through a content + * negotiation algorithm. + * @param exchange the current request + * @param producibleTypes the media types that can be produced for the current request + * @return the selected media type or {@code null} + */ + protected MediaType selectMediaType(ServerWebExchange exchange, List producibleTypes) { + + List acceptableTypes = getAcceptableTypes(exchange); + producibleTypes = getProducibleTypes(exchange, producibleTypes); + + Set compatibleMediaTypes = new LinkedHashSet<>(); + for (MediaType acceptable : acceptableTypes) { + for (MediaType producible : producibleTypes) { + if (acceptable.isCompatibleWith(producible)) { + compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible)); + } + } + } + + List result = new ArrayList<>(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(result); + + for (MediaType mediaType : compatibleMediaTypes) { + if (mediaType.isConcrete()) { + return mediaType; + } + else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) { + return MediaType.APPLICATION_OCTET_STREAM; + } + } + + return null; + } + + private List getAcceptableTypes(ServerWebExchange exchange) { + List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); + return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); + } + + private List getProducibleTypes(ServerWebExchange exchange, List mediaTypes) { + Optional optional = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (optional.isPresent()) { + Set set = (Set) optional.get(); + return new ArrayList<>(set); + } + return mediaTypes; + } + + private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) { + producible = producible.copyQualityValue(acceptable); + Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; + return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 8a64f6c86ce..a2107aa77db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -16,13 +16,8 @@ package org.springframework.web.reactive.result.method.annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import org.reactivestreams.Publisher; @@ -39,11 +34,11 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -53,25 +48,21 @@ import org.springframework.web.server.ServerWebExchange; * with {@code @ResponseBody} writing to the body of the request or response with * an {@link HttpMessageConverter}. * + *

By default the order for the result handler is set to 0. It is generally + * safe and expected it will be ordered ahead of other result handlers since it + * only gets involved based on the presence of an {@code @ResponseBody} + * annotation. + * * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze * @author Arjen Poutsma */ -public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { - - private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application"); +public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSupport + implements HandlerResultHandler, Ordered { private final List> messageConverters; - private final ConversionService conversionService; - - private final RequestedContentTypeResolver contentTypeResolver; - - private final List supportedMediaTypes; - - private int order = 0; - /** * Constructor with message converters and a {@code ConversionService} only @@ -91,46 +82,18 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered * Constructor with message converters, a {@code ConversionService}, and a * {@code RequestedContentTypeResolver}. * - * @param messageConverters converters for writing the response body with + * @param converters converters for writing the response body with * @param conversionService for converting other reactive types (e.g. * rx.Observable, rx.Single, etc.) to Flux or Mono + * @param contentTypeResolver for resolving the requested content type */ - public ResponseBodyResultHandler(List> messageConverters, + public ResponseBodyResultHandler(List> converters, ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { - Assert.notEmpty(messageConverters, "At least one message converter is required."); - Assert.notNull(conversionService, "'conversionService' is required."); - Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); - - this.messageConverters = messageConverters; - this.conversionService = conversionService; - this.contentTypeResolver = contentTypeResolver; - this.supportedMediaTypes = initSupportedMediaTypes(messageConverters); - } - - private static List initSupportedMediaTypes(List> converters) { - Set set = new LinkedHashSet<>(); - converters.forEach(converter -> set.addAll(converter.getWritableMediaTypes())); - List result = new ArrayList<>(set); - MediaType.sortBySpecificity(result); - return Collections.unmodifiableList(result); - } - - - /** - * Set the order for this result handler relative to others. - *

By default this is set to 0 and is generally save to be ahead of other - * result handlers since it only gets involved if the method (or class) is - * annotated with {@code @ResponseBody}. - * @param order the order - */ - public void setOrder(int order) { - this.order = order; - } - - @Override - public int getOrder() { - return this.order; + super(conversionService, contentTypeResolver); + Assert.notEmpty(converters, "At least one message converter is required."); + this.messageConverters = converters; + setOrder(0); } @@ -154,10 +117,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered ResolvableType elementType; ResolvableType returnType = result.getReturnValueType(); - if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { + if (getConversionService().canConvert(returnType.getRawClass(), Publisher.class)) { Optional optionalValue = result.getReturnValue(); if (optionalValue.isPresent()) { - publisher = this.conversionService.convert(optionalValue.get(), Publisher.class); + publisher = getConversionService().convert(optionalValue.get(), Publisher.class); } else { publisher = Mono.empty(); @@ -172,16 +135,9 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType; } - List compatibleMediaTypes = getCompatibleMediaTypes(exchange, elementType); - if (compatibleMediaTypes.isEmpty()) { - if (result.getReturnValue().isPresent()) { - List mediaTypes = getProducibleMediaTypes(exchange, elementType); - return Mono.error(new NotAcceptableStatusException(mediaTypes)); - } - return Mono.empty(); - } + List producibleTypes = getProducibleMediaTypes(elementType); + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); - MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes); if (bestMediaType != null) { for (HttpMessageConverter converter : this.messageConverters) { if (converter.canWrite(elementType, bestMediaType)) { @@ -191,64 +147,14 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } - return Mono.error(new NotAcceptableStatusException(this.supportedMediaTypes)); + return Mono.error(new NotAcceptableStatusException(producibleTypes)); } - private List getCompatibleMediaTypes(ServerWebExchange exchange, - ResolvableType elementType) { - - List acceptableMediaTypes = getAcceptableMediaTypes(exchange); - List producibleMediaTypes = getProducibleMediaTypes(exchange, elementType); - - Set compatibleMediaTypes = new LinkedHashSet<>(); - for (MediaType acceptable : acceptableMediaTypes) { - for (MediaType producible : producibleMediaTypes) { - if (acceptable.isCompatibleWith(producible)) { - compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible)); - } - } - } - - List result = new ArrayList<>(compatibleMediaTypes); - MediaType.sortBySpecificityAndQuality(result); - return result; - } - - private List getAcceptableMediaTypes(ServerWebExchange exchange) { - List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); - return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); - } - - private List getProducibleMediaTypes(ServerWebExchange exchange, ResolvableType type) { - Optional optional = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); - if (optional.isPresent()) { - Set mediaTypes = (Set) optional.get(); - return new ArrayList<>(mediaTypes); - } - else { - return this.messageConverters.stream() - .filter(converter -> converter.canWrite(type, null)) - .flatMap(converter -> converter.getWritableMediaTypes().stream()) - .collect(Collectors.toList()); - } - } - - private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) { - producible = producible.copyQualityValue(acceptable); - Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; - return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible); - } - - private MediaType selectBestMediaType(List compatibleMediaTypes) { - for (MediaType mediaType : compatibleMediaTypes) { - if (mediaType.isConcrete()) { - return mediaType; - } - else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) { - return MediaType.APPLICATION_OCTET_STREAM; - } - } - return null; + private List getProducibleMediaTypes(ResolvableType type) { + return this.messageConverters.stream() + .filter(converter -> converter.canWrite(type, null)) + .flatMap(converter -> converter.getWritableMediaTypes().stream()) + .collect(Collectors.toList()); } }