Fix "Content-Type" and "Accept" header processing

"Content-Type" is just a single MediaType.

For the response, the MediaType must be fully selected before
selecting and encoder.

The ResponseBodyResultHandler now includes actual content negotiation
with a potential 406 response.
This commit is contained in:
Rossen Stoyanchev 2015-11-13 15:47:29 -05:00
parent 2de127ad4a
commit 5d4201d500
2 changed files with 102 additions and 41 deletions

View File

@ -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<Object> 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<ByteBuffer> 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<MediaType> 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)) {

View File

@ -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<MediaType> allSupportedMediaTypes;
private final List<Encoder<?>> 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<MediaType> getAllSupportedMediaTypes(List<Encoder<?>> encoders) {
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<>();
for (Encoder<?> encoder : encoders) {
for (MimeType mimeType : encoder.getSupportedMimeTypes()) {
allSupportedMediaTypes.add(
new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()));
}
}
List<MediaType> 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<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> 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<MediaType> 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<ByteBuffer> outputStream = encoder.encode((Publisher) elementStream, returnType, mediaType);
if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
List<MimeType> mimeTypes = encoder.getSupportedMimeTypes();
if (!mimeTypes.isEmpty()) {
MimeType mimeType = mimeTypes.get(0);
mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters());
List<MediaType> 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<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN);
private List<MediaType> getAcceptableMediaTypes(ReactiveServerHttpRequest request) {
List<MediaType> mediaTypes = request.getHeaders().getAccept();
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}
private List<MediaType> getProducibleMediaTypes(ResolvableType type) {
List<MediaType> 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<MediaType> comparator = MediaType.SPECIFICITY_COMPARATOR;
return (comparator.compare(acceptType, produceType) <= 0 ? acceptType : produceType);
}
private Encoder<?> resolveEncoder(ResolvableType type, MediaType mediaType, Object... hints) {