diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 25e92c6899c..fab1615b350 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -25,7 +25,6 @@ import reactor.Publishers; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.codec.decoder.Decoder; @@ -59,7 +58,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve @Override public Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { - MediaType mediaType = resolveMediaType(request); + MediaType mediaType = request.getHeaders().getContentType(); + if (mediaType == null) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } ResolvableType type = ResolvableType.forMethodParameter(parameter); Publisher body = request.getBody(); Publisher elementStream = body; @@ -77,13 +79,6 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return Publishers.map(elementStream, element -> element); } - private MediaType resolveMediaType(ReactiveServerHttpRequest request) { - String acceptHeader = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); - List mediaTypes = MediaType.parseMediaTypes(acceptHeader); - MediaType.sortBySpecificityAndQuality(mediaTypes); - return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); - } - private Decoder resolveDecoder(ResolvableType type, MediaType mediaType, Object... hints) { for (Decoder decoder : this.decoders) { if (decoder.canDecode(type, mediaType, hints)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index dd4fd9aa49f..9567f7de48a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -17,18 +17,20 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.lang.reflect.Method; -import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.reactivestreams.Publisher; import reactor.Publishers; -import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.ConversionService; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -37,6 +39,7 @@ import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.util.Assert; import org.springframework.util.MimeType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; @@ -48,6 +51,10 @@ import org.springframework.web.method.HandlerMethod; */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { + private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); + + private final List allSupportedMediaTypes; + private final List> encoders; private final ConversionService conversionService; @@ -59,9 +66,23 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered Assert.notEmpty(encoders, "At least one encoders is required."); Assert.notNull(service, "'conversionService' is required."); this.encoders = encoders; + this.allSupportedMediaTypes = getAllSupportedMediaTypes(encoders); this.conversionService = service; } + private static List getAllSupportedMediaTypes(List> encoders) { + Set allSupportedMediaTypes = new LinkedHashSet<>(); + for (Encoder encoder : encoders) { + for (MimeType mimeType : encoder.getSupportedMimeTypes()) { + allSupportedMediaTypes.add( + new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters())); + } + } + List result = new ArrayList<>(allSupportedMediaTypes); + MediaType.sortBySpecificity(result); + return Collections.unmodifiableList(result); + } + public void setOrder(int order) { this.order = order; @@ -96,46 +117,91 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered HandlerMethod hm = (HandlerMethod) result.getHandler(); ResolvableType returnType = ResolvableType.forMethodParameter(hm.getReturnValueType(value)); - Publisher elementStream; - ResolvableType elementType; - if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { - elementStream = this.conversionService.convert(value, Publisher.class); - elementType = returnType.getGeneric(0); - } - else { - elementStream = Publishers.just(value); - elementType = returnType; + List requestedMediaTypes = getAcceptableMediaTypes(request); + List producibleMediaTypes = getProducibleMediaTypes(returnType); + + if (producibleMediaTypes.isEmpty()) { + Publishers.error(new IllegalArgumentException( + "No encoder found for return value of type: " + returnType)); } - MediaType mediaType = resolveMediaType(request); - Encoder encoder = resolveEncoder(elementType, mediaType); - if (encoder == null) { - return Publishers.error(new IllegalStateException( - "Return value type '" + returnType + - "' with media type '" + mediaType + "' not supported")); + Set compatibleMediaTypes = new LinkedHashSet<>(); + for (MediaType requestedType : requestedMediaTypes) { + for (MediaType producibleType : producibleMediaTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + if (compatibleMediaTypes.isEmpty()) { + return Publishers.error(new HttpMediaTypeNotAcceptableException(producibleMediaTypes)); } - Publisher outputStream = encoder.encode((Publisher) elementStream, returnType, mediaType); - if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { - List mimeTypes = encoder.getSupportedMimeTypes(); - if (!mimeTypes.isEmpty()) { - MimeType mimeType = mimeTypes.get(0); - mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); + List mediaTypes = new ArrayList<>(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(mediaTypes); + + MediaType selectedMediaType = null; + for (MediaType mediaType : mediaTypes) { + if (mediaType.isConcrete()) { + selectedMediaType = mediaType; + break; + } + else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { + selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; + break; } } - if (mediaType != null && !mediaType.equals(MediaType.ALL)) { - response.getHeaders().setContentType(mediaType); + if (selectedMediaType != null) { + Publisher publisher; + ResolvableType elementType; + if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { + publisher = this.conversionService.convert(value, Publisher.class); + elementType = returnType.getGeneric(0); + } + else { + publisher = Publishers.just(value); + elementType = returnType; + } + Encoder encoder = resolveEncoder(elementType, selectedMediaType); + if (encoder != null) { + response.getHeaders().setContentType(selectedMediaType); + return response.setBody(encoder.encode((Publisher) publisher, elementType, selectedMediaType)); + } } - return response.setBody(outputStream); + return Publishers.error(new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes)); } - private MediaType resolveMediaType(ReactiveServerHttpRequest request) { - String acceptHeader = request.getHeaders().getFirst(HttpHeaders.ACCEPT); - List mediaTypes = MediaType.parseMediaTypes(acceptHeader); - MediaType.sortBySpecificityAndQuality(mediaTypes); - return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); + private List getAcceptableMediaTypes(ReactiveServerHttpRequest request) { + List mediaTypes = request.getHeaders().getAccept(); + return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); + } + + private List getProducibleMediaTypes(ResolvableType type) { + List result = new ArrayList<>(); + for (Encoder encoder : this.encoders) { + if (encoder.canEncode(type, null)) { + for (MimeType mimeType : encoder.getSupportedMimeTypes()) { + result.add(new MediaType(mimeType.getType(), mimeType.getSubtype(), + mimeType.getParameters())); + } + } + } + if (result.isEmpty()) { + result.add(MediaType.ALL); + } + return result; + } + + /** + * Return the more specific of the acceptable and the producible media types + * with the q-value of the former. + */ + private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { + produceType = produceType.copyQualityValue(acceptType); + Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; + return (comparator.compare(acceptType, produceType) <= 0 ? acceptType : produceType); } private Encoder resolveEncoder(ResolvableType type, MediaType mediaType, Object... hints) {