Polish ResponseBody result handling
This commit is contained in:
parent
da3b21b020
commit
91d063899b
|
@ -42,10 +42,10 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
*/
|
||||
public class SimpleResultHandler implements Ordered, HandlerResultHandler {
|
||||
|
||||
private int order = Ordered.LOWEST_PRECEDENCE;
|
||||
|
||||
private ConversionService conversionService;
|
||||
|
||||
private int order = Ordered.LOWEST_PRECEDENCE;
|
||||
|
||||
|
||||
public SimpleResultHandler() {
|
||||
}
|
||||
|
|
|
@ -19,10 +19,8 @@ package org.springframework.web.reactive.result.method.annotation;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -40,7 +38,6 @@ import org.springframework.http.converter.reactive.HttpMessageConverter;
|
|||
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 org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
|
@ -50,6 +47,10 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
|
||||
|
||||
/**
|
||||
* {@code HandlerResultHandler} that handles return values from methods annotated
|
||||
* with {@code @ResponseBody} writing to the body of the request or response with
|
||||
* an {@link HttpMessageConverter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Stephane Maldini
|
||||
* @author Sebastien Deleuze
|
||||
|
@ -57,47 +58,48 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
*/
|
||||
public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered {
|
||||
|
||||
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
|
||||
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
|
||||
|
||||
private final List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private final List<MediaType> allMediaTypes;
|
||||
private final List<MediaType> supportedMediaTypes;
|
||||
|
||||
private final Map<HttpMessageConverter<?>, List<MediaType>> mediaTypesByEncoder;
|
||||
private int order = 0;
|
||||
|
||||
private int order = 0; // TODO: should be MAX_VALUE
|
||||
|
||||
/**
|
||||
* Constructor with message converters and conversion service.
|
||||
* @param messageConverters converters for writing the response body with
|
||||
* @param conversionService for converting to Flux and Mono from other reactive types
|
||||
*/
|
||||
public ResponseBodyResultHandler(List<HttpMessageConverter<?>> messageConverters,
|
||||
ConversionService service) {
|
||||
ConversionService conversionService) {
|
||||
|
||||
Assert.notEmpty(messageConverters, "At least one message converter is required.");
|
||||
Assert.notNull(service, "'conversionService' is required.");
|
||||
Assert.notNull(conversionService, "'conversionService' is required.");
|
||||
this.messageConverters = messageConverters;
|
||||
this.conversionService = service;
|
||||
this.allMediaTypes = getAllMediaTypes(messageConverters);
|
||||
this.mediaTypesByEncoder = getMediaTypesByConverter(messageConverters);
|
||||
this.conversionService = conversionService;
|
||||
this.supportedMediaTypes = initSupportedMediaTypes(messageConverters);
|
||||
}
|
||||
|
||||
private static List<MediaType> getAllMediaTypes(
|
||||
List<HttpMessageConverter<?>> messageConverters) {
|
||||
private static List<MediaType> initSupportedMediaTypes(List<HttpMessageConverter<?>> converters) {
|
||||
Set<MediaType> set = new LinkedHashSet<>();
|
||||
messageConverters.forEach(
|
||||
converter -> set.addAll(converter.getWritableMediaTypes()));
|
||||
converters.forEach(converter -> set.addAll(converter.getWritableMediaTypes()));
|
||||
List<MediaType> result = new ArrayList<>(set);
|
||||
MediaType.sortBySpecificity(result);
|
||||
return Collections.unmodifiableList(result);
|
||||
}
|
||||
|
||||
private static Map<HttpMessageConverter<?>, List<MediaType>> getMediaTypesByConverter(
|
||||
List<HttpMessageConverter<?>> converters) {
|
||||
Map<HttpMessageConverter<?>, List<MediaType>> result =
|
||||
new HashMap<>(converters.size());
|
||||
converters.forEach(converter -> result
|
||||
.put(converter, converter.getWritableMediaTypes()));
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order for this result handler relative to others.
|
||||
* <p>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;
|
||||
}
|
||||
|
@ -144,27 +146,23 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
|
|||
elementType = returnType;
|
||||
}
|
||||
|
||||
List<MediaType> compatibleMediaTypes =
|
||||
getCompatibleMediaTypes(exchange.getRequest(), elementType);
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
List<MediaType> compatibleMediaTypes = getCompatibleMediaTypes(request, elementType);
|
||||
if (compatibleMediaTypes.isEmpty()) {
|
||||
return Mono.error(new NotAcceptableStatusException(
|
||||
getProducibleMediaTypes(elementType)));
|
||||
List<MediaType> supported = getProducibleMediaTypes(elementType);
|
||||
return Mono.error(new NotAcceptableStatusException(supported));
|
||||
}
|
||||
|
||||
Optional<MediaType> selectedMediaType = selectBestMediaType(compatibleMediaTypes);
|
||||
|
||||
if (selectedMediaType.isPresent()) {
|
||||
HttpMessageConverter<?> converter =
|
||||
resolveEncoder(elementType, selectedMediaType.get());
|
||||
MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes);
|
||||
if (bestMediaType != null) {
|
||||
HttpMessageConverter<?> converter = resolveEncoder(elementType, bestMediaType);
|
||||
if (converter != null) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
return converter.write((Publisher) publisher, elementType,
|
||||
selectedMediaType.get(),
|
||||
response);
|
||||
return converter.write((Publisher) publisher, elementType, bestMediaType, response);
|
||||
}
|
||||
}
|
||||
|
||||
return Mono.error(new NotAcceptableStatusException(this.allMediaTypes));
|
||||
return Mono.error(new NotAcceptableStatusException(this.supportedMediaTypes));
|
||||
}
|
||||
|
||||
private List<MediaType> getCompatibleMediaTypes(ServerHttpRequest request,
|
||||
|
@ -174,11 +172,12 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
|
|||
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(elementType);
|
||||
|
||||
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
|
||||
for (MediaType acceptableMediaType : acceptableMediaTypes) {
|
||||
compatibleMediaTypes.addAll(producibleMediaTypes.stream().
|
||||
filter(acceptableMediaType::isCompatibleWith).
|
||||
map(producibleType -> getMostSpecificMediaType(acceptableMediaType,
|
||||
producibleType)).collect(Collectors.toList()));
|
||||
for (MediaType acceptable : acceptableMediaTypes) {
|
||||
for (MediaType producible : producibleMediaTypes) {
|
||||
if (acceptable.isCompatibleWith(producible)) {
|
||||
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<MediaType> result = new ArrayList<>(compatibleMediaTypes);
|
||||
|
@ -191,44 +190,37 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
|
|||
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
|
||||
}
|
||||
|
||||
private Optional<MediaType> selectBestMediaType(
|
||||
List<MediaType> compatibleMediaTypes) {
|
||||
private List<MediaType> getProducibleMediaTypes(ResolvableType type) {
|
||||
return this.messageConverters.stream()
|
||||
.filter(converter -> converter.canWrite(type, null))
|
||||
.flatMap(converter -> converter.getWritableMediaTypes().stream())
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), result -> {
|
||||
if (result.isEmpty()) {
|
||||
result.add(MediaType.ALL);
|
||||
}
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
private MediaType getMostSpecificMediaType(MediaType acceptable, MediaType producible) {
|
||||
producible = producible.copyQualityValue(acceptable);
|
||||
Comparator<MediaType> comparator = MediaType.SPECIFICITY_COMPARATOR;
|
||||
return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
|
||||
}
|
||||
|
||||
private MediaType selectBestMediaType(List<MediaType> compatibleMediaTypes) {
|
||||
for (MediaType mediaType : compatibleMediaTypes) {
|
||||
if (mediaType.isConcrete()) {
|
||||
return Optional.of(mediaType);
|
||||
return mediaType;
|
||||
}
|
||||
else if (mediaType.equals(MediaType.ALL) ||
|
||||
mediaType.equals(MEDIA_TYPE_APPLICATION)) {
|
||||
return Optional.of(MediaType.APPLICATION_OCTET_STREAM);
|
||||
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) {
|
||||
return MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<MediaType> getProducibleMediaTypes(ResolvableType type) {
|
||||
List<MediaType> result = this.messageConverters.stream()
|
||||
.filter(converter -> converter.canWrite(type, null))
|
||||
.flatMap(encoder -> this.mediaTypesByEncoder.get(encoder).stream())
|
||||
.collect(Collectors.toList());
|
||||
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 HttpMessageConverter<?> resolveEncoder(ResolvableType type,
|
||||
MediaType mediaType) {
|
||||
private HttpMessageConverter<?> resolveEncoder(ResolvableType type, MediaType mediaType) {
|
||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||
if (converter.canWrite(type, mediaType)) {
|
||||
return converter;
|
||||
|
|
|
@ -53,7 +53,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered
|
|||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private int order = Integer.MAX_VALUE;
|
||||
private int order = Ordered.LOWEST_PRECEDENCE;
|
||||
|
||||
|
||||
public ViewResolverResultHandler(List<ViewResolver> resolvers, ConversionService service) {
|
||||
|
@ -87,17 +87,17 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered
|
|||
@Override
|
||||
public boolean supports(HandlerResult result) {
|
||||
Class<?> clazz = result.getReturnValueType().getRawClass();
|
||||
if (isViewNameOrViewReference(clazz)) {
|
||||
if (isStringOrViewReference(clazz)) {
|
||||
return true;
|
||||
}
|
||||
if (this.conversionService.canConvert(clazz, Mono.class)) {
|
||||
clazz = result.getReturnValueType().getGeneric(0).getRawClass();
|
||||
return isViewNameOrViewReference(clazz);
|
||||
return isStringOrViewReference(clazz);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isViewNameOrViewReference(Class<?> clazz) {
|
||||
private boolean isStringOrViewReference(Class<?> clazz) {
|
||||
return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz));
|
||||
}
|
||||
|
||||
|
|
|
@ -16,37 +16,44 @@
|
|||
|
||||
package org.springframework.web.reactive.result.method.annotation;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.support.StringEncoder;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
|
||||
import org.springframework.http.converter.reactive.HttpMessageConverter;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResponseBodyResultHandler}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ResponseBodyResultHandlerTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void supports() throws NoSuchMethodException {
|
||||
ResponseBodyResultHandler handler = new ResponseBodyResultHandler(Collections.singletonList(
|
||||
new CodecHttpMessageConverter<String>(new StringEncoder(), null)),
|
||||
new DefaultConversionService());
|
||||
ResponseBodyResultHandler handler = createResultHandler(new StringEncoder());
|
||||
TestController controller = new TestController();
|
||||
|
||||
HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated"));
|
||||
HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated"));
|
||||
ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap())));
|
||||
|
||||
|
@ -59,6 +66,21 @@ public class ResponseBodyResultHandlerTests {
|
|||
assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOrder() throws Exception {
|
||||
ResponseBodyResultHandler handler = createResultHandler(new StringEncoder());
|
||||
assertEquals(0, handler.getOrder());
|
||||
}
|
||||
|
||||
|
||||
private ResponseBodyResultHandler createResultHandler(Encoder<?>... encoders) {
|
||||
List<HttpMessageConverter<?>> converters = Arrays.stream(encoders)
|
||||
.map(encoder -> new CodecHttpMessageConverter<>(encoder, null))
|
||||
.collect(Collectors.toList());
|
||||
return new ResponseBodyResultHandler(converters, new DefaultConversionService());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
|
Loading…
Reference in New Issue