提交 1e1e2f8b 编写于 作者: R Rossen Stoyanchev

Support HttpEntity method arguments

The RequestBodyArgumentResolver has been refactored to have a shared
base class and tests with the new HttpEntityMethodArgumentResolver.

An HttpEntity argument is not expected to have an async wrapper because
the request headers are available immediately. The body however can be
asynchronous, e.g. HttpEntity<Flux<String>>.
上级 4c3c5446
/*
* 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.method.annotation;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
/**
* Abstract base class for argument resolvers that resolve method arguments
* by reading the request body with an {@link HttpMessageConverter}.
*
* <p>Applies validation if the method argument is annotated with
* {@code @javax.validation.Valid} or
* {@link org.springframework.validation.annotation.Validated}. Validation
* failure results in an {@link ServerWebInputException}.
*
* @author Rossen Stoyanchev
*/
public abstract class AbstractMessageConverterArgumentResolver {
private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageConverter<?>> messageConverters;
private final ConversionService conversionService;
private final Validator validator;
private final List<MediaType> supportedMediaTypes;
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with
*/
protected AbstractMessageConverterArgumentResolver(List<HttpMessageConverter<?>> converters,
ConversionService service, Validator validator) {
Assert.notEmpty(converters, "At least one message converter is required.");
Assert.notNull(service, "'conversionService' is required.");
this.messageConverters = converters;
this.conversionService = service;
this.validator = validator;
this.supportedMediaTypes = converters.stream()
.flatMap(converter -> converter.getReadableMediaTypes().stream())
.collect(Collectors.toList());
}
/**
* Return the configured message converters.
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;
}
/**
* Return the configured {@link ConversionService}.
*/
public ConversionService getConversionService() {
return this.conversionService;
}
protected Mono<Object> readBody(MethodParameter bodyParameter, ServerWebExchange exchange) {
TypeDescriptor typeDescriptor = new TypeDescriptor(bodyParameter);
boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor);
boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor);
ResolvableType elementType = ResolvableType.forMethodParameter(bodyParameter);
if (convertFromMono || convertFromFlux) {
elementType = elementType.getGeneric(0);
}
ServerHttpRequest request = exchange.getRequest();
MediaType mediaType = request.getHeaders().getContentType();
if (mediaType == null) {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
for (HttpMessageConverter<?> converter : getMessageConverters()) {
if (converter.canRead(elementType, mediaType)) {
if (convertFromFlux) {
Flux<?> flux = converter.read(elementType, request);
if (this.validator != null) {
flux = flux.map(applyValidationIfApplicable(bodyParameter));
}
return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor));
}
else {
Mono<?> mono = converter.readMono(elementType, request);
if (this.validator != null) {
mono = mono.map(applyValidationIfApplicable(bodyParameter));
}
if (convertFromMono) {
return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor));
}
else {
return Mono.from(mono);
}
}
}
}
return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes));
}
protected <T> Function<T, T> applyValidationIfApplicable(MethodParameter methodParam) {
Annotation[] annotations = methodParam.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann));
Object[] validHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
return element -> {
doValidate(element, validHints, methodParam);
return element;
};
}
}
return element -> element;
}
/**
* TODO: replace with use of DataBinder
*/
private void doValidate(Object target, Object[] validationHints, MethodParameter methodParam) {
String name = Conventions.getVariableNameForParameter(methodParam);
Errors errors = new BeanPropertyBindingResult(target, name);
if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) {
((SmartValidator) this.validator).validate(target, errors, validationHints);
}
else if (this.validator != null) {
this.validator.validate(target, errors);
}
if (errors.hasErrors()) {
throw new ServerWebInputException("Validation failed", methodParam);
}
}
}
/*
* 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.method.annotation;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.ui.ModelMap;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.server.ServerWebExchange;
/**
* Resolves method arguments of type {@link HttpEntity} or {@link RequestEntity}
* by reading the body of the request through a compatible
* {@code HttpMessageConverter}.
*
* @author Rossen Stoyanchev
*/
public class HttpEntityArgumentResolver extends AbstractMessageConverterArgumentResolver
implements HandlerMethodArgumentResolver {
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param service for converting to other reactive types from Flux and Mono
*/
public HttpEntityArgumentResolver(List<HttpMessageConverter<?>> converters,
ConversionService service) {
this(converters, service, null);
}
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with
*/
public HttpEntityArgumentResolver(List<HttpMessageConverter<?>> converters,
ConversionService service, Validator validator) {
super(converters, service, validator);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return (HttpEntity.class.equals(clazz) || RequestEntity.class.equals(clazz));
}
@Override
public Mono<Object> resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) {
ResolvableType entityType;
MethodParameter bodyParameter;
if (getConversionService().canConvert(Mono.class, param.getParameterType())) {
entityType = ResolvableType.forMethodParameter(param).getGeneric(0);
bodyParameter = new MethodParameter(param);
bodyParameter.increaseNestingLevel();
bodyParameter.increaseNestingLevel();
}
else {
entityType = ResolvableType.forMethodParameter(param);
bodyParameter = new MethodParameter(param);
bodyParameter.increaseNestingLevel();
}
return readBody(bodyParameter, exchange)
.map(body -> {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
if (RequestEntity.class == entityType.getRawClass()) {
return new RequestEntity<>(body, headers, request.getMethod(), request.getURI());
}
else {
return new HttpEntity<>(body, headers);
}
});
}
}
......@@ -16,60 +16,36 @@
package org.springframework.web.reactive.result.method.annotation;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
/**
* Resolves method arguments annotated with {@code @RequestBody} by reading and
* decoding the body of the request through a compatible
* {@code HttpMessageConverter}.
* Resolves method arguments annotated with {@code @RequestBody} by reading the
* body of the request through a compatible {@code HttpMessageConverter}.
*
* <p>An {@code @RequestBody} method argument is also validated if it is
* annotated with {@code @javax.validation.Valid} or
* {@link org.springframework.validation.annotation.Validated}. Validation
* failure results in an {@link ServerWebInputException}.
*
* @author Sebastien Deleuze
* @author Stephane Maldini
* @author Rossen Stoyanchev
*/
public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageConverter<?>> messageConverters;
private final ConversionService conversionService;
private final Validator validator;
private final List<MediaType> supportedMediaTypes;
public class RequestBodyArgumentResolver extends AbstractMessageConverterArgumentResolver
implements HandlerMethodArgumentResolver {
/**
......@@ -92,29 +68,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
public RequestBodyArgumentResolver(List<HttpMessageConverter<?>> converters,
ConversionService service, Validator validator) {
Assert.notEmpty(converters, "At least one message converter is required.");
Assert.notNull(service, "'conversionService' is required.");
this.messageConverters = converters;
this.conversionService = service;
this.validator = validator;
this.supportedMediaTypes = converters.stream()
.flatMap(converter -> converter.getReadableMediaTypes().stream())
.collect(Collectors.toList());
}
/**
* Return the configured message converters.
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;
}
/**
* Return the configured {@link ConversionService}.
*/
public ConversionService getConversionService() {
return this.conversionService;
super(converters, service, validator);
}
......@@ -124,79 +78,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) {
TypeDescriptor typeDescriptor = new TypeDescriptor(parameter);
boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor);
boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor);
ResolvableType type = ResolvableType.forMethodParameter(parameter);
ResolvableType elementType = convertFromMono || convertFromFlux ? type.getGeneric(0) : type;
ServerHttpRequest request = exchange.getRequest();
MediaType mediaType = request.getHeaders().getContentType();
if (mediaType == null) {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
for (HttpMessageConverter<?> converter : getMessageConverters()) {
if (converter.canRead(elementType, mediaType)) {
if (convertFromFlux) {
Flux<?> flux = converter.read(elementType, request);
if (this.validator != null) {
flux = flux.map(applyValidationIfApplicable(parameter));
}
return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor));
}
else {
Mono<?> mono = converter.readMono(elementType, request);
if (this.validator != null) {
mono = mono.map(applyValidationIfApplicable(parameter));
}
if (convertFromMono) {
return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor));
}
else {
return Mono.from(mono);
}
}
}
}
return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes));
}
protected <T> Function<T, T> applyValidationIfApplicable(MethodParameter methodParam) {
Annotation[] annotations = methodParam.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann));
Object[] validHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
return element -> {
doValidate(element, validHints, methodParam);
return element;
};
}
}
return element -> element;
}
/**
* TODO: replace with use of DataBinder
*/
private void doValidate(Object target, Object[] validationHints, MethodParameter methodParam) {
String name = Conventions.getVariableNameForParameter(methodParam);
Errors errors = new BeanPropertyBindingResult(target, name);
if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) {
((SmartValidator) this.validator).validate(target, errors, validationHints);
}
else if (this.validator != null) {
this.validator.validate(target, errors);
}
if (errors.hasErrors()) {
throw new ServerWebInputException("Validation failed", methodParam);
}
public Mono<Object> resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) {
return readBody(param, exchange);
}
}
/*
* 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.method.annotation;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.test.TestSubscriber;
import rx.Observable;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/**
* Unit tests for {@link HttpEntityArgumentResolver}.When adding a test also
* consider whether the logic under test is in a parent class, then see:
* {@link MessageConverterArgumentResolverTests}.
*
* @author Rossen Stoyanchev
*/
public class HttpEntityArgumentResolverTests {
private HttpEntityArgumentResolver resolver = resolver();
private ServerWebExchange exchange;
private MockServerHttpRequest request;
private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle");
@Before
public void setUp() throws Exception {
this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path"));
MockServerHttpResponse response = new MockServerHttpResponse();
this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager());
}
@Test
public void supports() throws Exception {
testSupports(httpEntity(String.class));
testSupports(httpEntity(forClassWithGenerics(Mono.class, String.class)));
testSupports(httpEntity(forClassWithGenerics(Single.class, String.class)));
testSupports(httpEntity(forClassWithGenerics(CompletableFuture.class, String.class)));
testSupports(httpEntity(forClassWithGenerics(Flux.class, String.class)));
testSupports(httpEntity(forClassWithGenerics(Observable.class, String.class)));
testSupports(forClassWithGenerics(RequestEntity.class, String.class));
}
@Test
public void doesNotSupport() throws Exception {
ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, String.class);
assertFalse(this.resolver.supportsParameter(this.testMethod.resolveParam(type)));
type = ResolvableType.forClass(String.class);
assertFalse(this.resolver.supportsParameter(this.testMethod.resolveParam(type)));
}
@Test
public void httpEntityWithStringBody() throws Exception {
String body = "line1";
ResolvableType type = httpEntity(String.class);
HttpEntity<String> httpEntity = resolveValue(type, body);
assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
assertEquals("line1", httpEntity.getBody());
}
@Test
public void httpEntityWithMonoBody() throws Exception {
String body = "line1";
ResolvableType type = httpEntity(forClassWithGenerics(Mono.class, String.class));
HttpEntity<Mono<String>> httpEntity = resolveValue(type, body);
assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
assertEquals("line1", httpEntity.getBody().block());
}
@Test
public void httpEntityWithSingleBody() throws Exception {
String body = "line1";
ResolvableType type = httpEntity(forClassWithGenerics(Single.class, String.class));
HttpEntity<Single<String>> httpEntity = resolveValue(type, body);
assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
assertEquals("line1", httpEntity.getBody().toBlocking().value());
}
@Test
public void httpEntityWithCompletableFutureBody() throws Exception {
String body = "line1";
ResolvableType type = httpEntity(forClassWithGenerics(CompletableFuture.class, String.class));
HttpEntity<CompletableFuture<String>> httpEntity = resolveValue(type, body);
assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
assertEquals("line1", httpEntity.getBody().get());
}
@Test
public void httpEntityWithFluxBody() throws Exception {
String body = "line1\nline2\nline3\n";
ResolvableType type = httpEntity(forClassWithGenerics(Flux.class, String.class));
HttpEntity<Flux<String>> httpEntity = resolveValue(type, body);
assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
TestSubscriber.subscribe(httpEntity.getBody()).assertValues("line1\n", "line2\n", "line3\n");
}
@Test
public void requestEntity() throws Exception {
String body = "line1";
ResolvableType type = forClassWithGenerics(RequestEntity.class, String.class);
RequestEntity<String> requestEntity = resolveValue(type, body);
assertEquals(this.request.getMethod(), requestEntity.getMethod());
assertEquals(this.request.getURI(), requestEntity.getUrl());
assertEquals(this.request.getHeaders(), requestEntity.getHeaders());
assertEquals("line1", requestEntity.getBody());
}
private ResolvableType httpEntity(Class<?> bodyType) {
return httpEntity(ResolvableType.forClass(bodyType));
}
private ResolvableType httpEntity(ResolvableType type) {
return forClassWithGenerics(HttpEntity.class, type);
}
private HttpEntityArgumentResolver resolver() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new HttpEntityArgumentResolver(converters, service);
}
private void testSupports(ResolvableType type) {
MethodParameter parameter = this.testMethod.resolveParam(type);
assertTrue(this.resolver.supportsParameter(parameter));
}
@SuppressWarnings("unchecked")
private <T> T resolveValue(ResolvableType type, String body) {
this.request.getHeaders().add("foo", "bar");
this.request.getHeaders().setContentType(MediaType.TEXT_PLAIN);
this.request.writeWith(Flux.just(dataBuffer(body)));
MethodParameter param = this.testMethod.resolveParam(type);
Mono<Object> result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange);
Object value = result.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue("Unexpected return value type: " + value.getClass(),
param.getParameterType().isAssignableFrom(value.getClass()));
return (T) value;
}
private DataBuffer dataBuffer(String body) {
byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
return new DefaultDataBufferFactory().wrap(byteBuffer);
}
@SuppressWarnings("unused")
void handle(
String string,
Mono<String> monoString,
HttpEntity<String> httpEntity,
HttpEntity<Mono<String>> monoBody,
HttpEntity<Single<String>> singleBody,
HttpEntity<CompletableFuture<String>> completableFutureBody,
HttpEntity<Flux<String>> fluxBody,
HttpEntity<Observable<String>> observableBody,
RequestEntity<String> requestEntity) {}
}
/*
* 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.method.annotation;
import java.io.Serializable;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.xml.bind.annotation.XmlRootElement;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.test.TestSubscriber;
import rx.Observable;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/**
* Unit tests for {@link AbstractMessageConverterArgumentResolver}.
* @author Rossen Stoyanchev
*/
public class MessageConverterArgumentResolverTests {
private AbstractMessageConverterArgumentResolver resolver = resolver(new JacksonJsonDecoder());
private ServerWebExchange exchange;
private MockServerHttpRequest request;
private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle");
@Before
public void setUp() throws Exception {
this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
MockServerHttpResponse response = new MockServerHttpResponse();
this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager());
}
@Test
public void missingContentType() throws Exception {
String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}";
this.request.writeWith(Flux.just(dataBuffer(body)));
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<Object> result = this.resolver.readBody(param, this.exchange);
TestSubscriber.subscribe(result)
.assertError(UnsupportedMediaTypeStatusException.class);
}
@Test // SPR-9942
public void noContent() throws Exception {
this.request.writeWith(Flux.empty());
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<Object> result = this.resolver.readBody(param, this.exchange);
TestSubscriber.subscribe(result).assertError(UnsupportedMediaTypeStatusException.class);
}
@Test
public void monoTestBean() throws Exception {
String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}";
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<Object> mono = resolveValue(param, body);
assertEquals(new TestBean("FOOFOO", "BARBAR"), mono.block());
}
@Test
public void fluxTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Flux<TestBean> flux = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
flux.collectList().block());
}
@Test
public void singleTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(Single.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Single<TestBean> single = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), single.toBlocking().value());
}
@Test
public void observableTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Observable.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Observable<?> observable = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
observable.toList().toBlocking().first());
}
@Test
public void futureTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(CompletableFuture.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
CompletableFuture<?> future = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), future.get());
}
@Test
public void testBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
MethodParameter param = this.testMethod.resolveParam(forClass(TestBean.class));
TestBean value = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), value);
}
@Test
public void map() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
Map<String, String> map = new HashMap<>();
map.put("foo", "f1");
map.put("bar", "b1");
ResolvableType type = forClassWithGenerics(Map.class, String.class, String.class);
MethodParameter param = this.testMethod.resolveParam(type);
Map actual = resolveValue(param, body);
assertEquals(map, actual);
}
@Test
public void list() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(List.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
List<?> list = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list);
}
@Test
public void monoList() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Mono.class, forClassWithGenerics(List.class, TestBean.class));
MethodParameter param = this.testMethod.resolveParam(type);
Mono<?> mono = resolveValue(param, body);
List<?> list = (List<?>) mono.block(Duration.ofSeconds(5));
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list);
}
@Test
public void array() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClass(TestBean[].class);
MethodParameter param = this.testMethod.resolveParam(type);
TestBean[] value = resolveValue(param, body);
assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, value);
}
@Test @SuppressWarnings("unchecked")
public void validateMonoTestBean() throws Exception {
String body = "{\"bar\":\"b1\"}";
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<TestBean> mono = resolveValue(param, body);
TestSubscriber.subscribe(mono)
.assertNoValues()
.assertError(ServerWebInputException.class);
}
@Test @SuppressWarnings("unchecked")
public void validateFluxTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]";
ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Flux<TestBean> flux = resolveValue(param, body);
TestSubscriber.subscribe(flux)
.assertValues(new TestBean("f1", "b1"))
.assertError(ServerWebInputException.class);
}
@Test // SPR-9964
@Ignore
public void parameterizedMethodArgument() throws Exception {
Class<?> clazz = ConcreteParameterizedController.class;
MethodParameter param = ResolvableMethod.on(clazz).name("handleDto").resolveParam();
SimpleBean simpleBean = resolveValue(param, "{\"name\" : \"Jad\"}");
assertEquals("Jad", simpleBean.getName());
}
@SuppressWarnings("unchecked")
private <T> T resolveValue(MethodParameter param, String body) {
this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
this.request.writeWith(Flux.just(dataBuffer(body)));
Mono<Object> result = this.resolver.readBody(param, this.exchange);
Object value = result.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue("Unexpected return value type: " + value,
param.getParameterType().isAssignableFrom(value.getClass()));
return (T) value;
}
@SuppressWarnings("Convert2MethodRef")
private AbstractMessageConverterArgumentResolver resolver(Decoder<?>... decoders) {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder)));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new AbstractMessageConverterArgumentResolver(converters, service, new TestBeanValidator()) {};
}
private DataBuffer dataBuffer(String body) {
byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
return new DefaultDataBufferFactory().wrap(byteBuffer);
}
@SuppressWarnings("unused")
private void handle(
@Validated Mono<TestBean> monoTestBean,
@Validated Flux<TestBean> fluxTestBean,
Single<TestBean> singleTestBean,
Observable<TestBean> observableTestBean,
CompletableFuture<TestBean> futureTestBean,
TestBean testBean,
Map<String, String> map,
List<TestBean> list,
Mono<List<TestBean>> monoList,
Set<TestBean> set,
TestBean[] array) {}
@XmlRootElement
private static class TestBean {
private String foo;
private String bar;
@SuppressWarnings("unused")
public TestBean() {
}
TestBean(String foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof TestBean) {
TestBean other = (TestBean) o;
return this.foo.equals(other.foo) && this.bar.equals(other.bar);
}
return false;
}
@Override
public int hashCode() {
return 31 * foo.hashCode() + bar.hashCode();
}
@Override
public String toString() {
return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']";
}
}
private static class TestBeanValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(TestBean.class);
}
@Override
public void validate(Object target, Errors errors) {
TestBean testBean = (TestBean) target;
if (testBean.getFoo() == null) {
errors.rejectValue("foo", "nullValue");
}
}
}
private static abstract class AbstractParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(DTO dto) {}
}
private static class ConcreteParameterizedController extends AbstractParameterizedController<SimpleBean> {
}
private interface Identifiable extends Serializable {
Long getId();
void setId(Long id);
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name;
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
......@@ -15,430 +15,65 @@
*/
package org.springframework.web.reactive.result.method.annotation;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.xml.bind.annotation.XmlRootElement;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.test.TestSubscriber;
import rx.Observable;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/**
* Unit tests for {@link RequestBodyArgumentResolver}.
* Unit tests for {@link RequestBodyArgumentResolver}.When adding a test also
* consider whether the logic under test is in a parent class, then see:
* {@link MessageConverterArgumentResolverTests}.
*
* @author Rossen Stoyanchev
*/
public class RequestBodyArgumentResolverTests {
private RequestBodyArgumentResolver resolver = resolver(new JacksonJsonDecoder());
private ServerWebExchange exchange;
private MockServerHttpRequest request;
private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle");
@Before
public void setUp() throws Exception {
this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
MockServerHttpResponse response = new MockServerHttpResponse();
this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager());
}
@Test
public void supports() throws Exception {
RequestBodyArgumentResolver resolver = resolver(new StringDecoder());
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle");
RequestBodyArgumentResolver resolver = resolver();
ResolvableType type = forClassWithGenerics(Mono.class, String.class);
MethodParameter param = testMethod.resolveParam(type);
assertTrue(resolver.supportsParameter(param));
MethodParameter parameter = this.testMethod.resolveParam(p -> !p.hasParameterAnnotations());
MethodParameter parameter = testMethod.resolveParam(p -> !p.hasParameterAnnotations());
assertFalse(resolver.supportsParameter(parameter));
}
@Test
public void missingContentType() throws Exception {
String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}";
this.request.writeWith(Flux.just(dataBuffer(body)));
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<Object> result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange);
TestSubscriber.subscribe(result)
.assertError(UnsupportedMediaTypeStatusException.class);
}
@Test // SPR-9942
public void missingContent() throws Exception {
this.request.writeWith(Flux.empty());
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<Object> result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange);
TestSubscriber.subscribe(result)
.assertError(UnsupportedMediaTypeStatusException.class);
}
@Test @SuppressWarnings("unchecked")
public void monoTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<TestBean> mono = (Mono<TestBean>) resolveValue(param, Mono.class, body);
assertEquals(new TestBean("f1", "b1"), mono.block());
}
@Test @SuppressWarnings("unchecked")
public void fluxTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Flux<TestBean> flux = (Flux<TestBean>) resolveValue(param, Flux.class, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
flux.collectList().block());
}
@Test @SuppressWarnings("unchecked")
public void singleTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(Single.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Single<TestBean> single = (Single<TestBean>) resolveValue(param, Single.class, body);
assertEquals(new TestBean("f1", "b1"), single.toBlocking().value());
}
@Test @SuppressWarnings("unchecked")
public void observableTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Observable.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Observable<?> observable = (Observable<?>) resolveValue(param, Observable.class, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
observable.toList().toBlocking().first());
}
@Test @SuppressWarnings("unchecked")
public void futureTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(CompletableFuture.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
CompletableFuture future = resolveValue(param, CompletableFuture.class, body);
assertEquals(new TestBean("f1", "b1"), future.get());
}
@Test
public void testBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
MethodParameter param = this.testMethod.resolveParam(
forClass(TestBean.class), p -> p.hasParameterAnnotation(RequestBody.class));
TestBean value = resolveValue(param, TestBean.class, body);
assertEquals(new TestBean("f1", "b1"), value);
}
@Test
public void map() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
Map<String, String> map = new HashMap<>();
map.put("foo", "f1");
map.put("bar", "b1");
ResolvableType type = forClassWithGenerics(Map.class, String.class, String.class);
MethodParameter param = this.testMethod.resolveParam(type);
Map actual = resolveValue(param, Map.class, body);
assertEquals(map, actual);
}
@Test
public void list() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(List.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
List<?> list = resolveValue(param, List.class, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list);
}
@Test
public void monoList() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Mono.class, forClassWithGenerics(List.class, TestBean.class));
MethodParameter param = this.testMethod.resolveParam(type);
Mono<?> mono = resolveValue(param, Mono.class, body);
List<?> list = (List<?>) mono.block(Duration.ofSeconds(5));
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list);
}
@Test
public void array() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClass(TestBean[].class);
MethodParameter param = this.testMethod.resolveParam(type);
TestBean[] value = resolveValue(param, TestBean[].class, body);
assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, value);
}
@Test @SuppressWarnings("unchecked")
public void validateMonoTestBean() throws Exception {
String body = "{\"bar\":\"b1\"}";
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Mono<TestBean> mono = resolveValue(param, Mono.class, body);
TestSubscriber.subscribe(mono)
.assertNoValues()
.assertError(ServerWebInputException.class);
}
@Test @SuppressWarnings("unchecked")
public void validateFluxTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]";
ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class);
MethodParameter param = this.testMethod.resolveParam(type);
Flux<TestBean> flux = resolveValue(param, Flux.class, body);
TestSubscriber.subscribe(flux)
.assertValues(new TestBean("f1", "b1"))
.assertError(ServerWebInputException.class);
}
@Test // SPR-9964
@Ignore
public void parameterizedMethodArgument() throws Exception {
Class<?> clazz = ConcreteParameterizedController.class;
MethodParameter param = ResolvableMethod.on(clazz).name("handleDto").resolveParam();
SimpleBean simpleBean = resolveValue(param, SimpleBean.class, "{\"name\" : \"Jad\"}");
assertEquals("Jad", simpleBean.getName());
}
@SuppressWarnings("unchecked")
private <T> T resolveValue(MethodParameter param, Class<T> valueType, String body) {
this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
this.request.writeWith(Flux.just(dataBuffer(body)));
Mono<Object> result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange);
Object value = result.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue("Unexpected return value type: " + value, valueType.isAssignableFrom(value.getClass()));
return (T) value;
}
@SuppressWarnings("Convert2MethodRef")
private RequestBodyArgumentResolver resolver(Decoder<?>... decoders) {
private RequestBodyArgumentResolver resolver() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder)));
converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator());
}
private DataBuffer dataBuffer(String body) {
byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
return new DefaultDataBufferFactory().wrap(byteBuffer);
return new RequestBodyArgumentResolver(converters, service);
}
@SuppressWarnings("unused")
void handle(
@Validated @RequestBody Mono<TestBean> monoTestBean,
@Validated @RequestBody Flux<TestBean> fluxTestBean,
@RequestBody Single<TestBean> singleTestBean,
@RequestBody Observable<TestBean> observableTestBean,
@RequestBody CompletableFuture<TestBean> futureTestBean,
@RequestBody TestBean testBean,
@RequestBody Map<String, String> map,
@RequestBody List<TestBean> list,
@RequestBody Mono<List<TestBean>> monoList,
@RequestBody Set<TestBean> set,
@RequestBody TestBean[] array,
TestBean paramWithoutAnnotation) {
}
@XmlRootElement
private static class TestBean {
private String foo;
private String bar;
@SuppressWarnings("unused")
public TestBean() {
}
TestBean(String foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof TestBean) {
TestBean other = (TestBean) o;
return this.foo.equals(other.foo) && this.bar.equals(other.bar);
}
return false;
}
@Override
public int hashCode() {
return 31 * foo.hashCode() + bar.hashCode();
}
@Override
public String toString() {
return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']";
}
}
private static class TestBeanValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(TestBean.class);
}
@Override
public void validate(Object target, Errors errors) {
TestBean testBean = (TestBean) target;
if (testBean.getFoo() == null) {
errors.rejectValue("foo", "nullValue");
}
}
}
private static abstract class AbstractParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(@RequestBody DTO dto) {}
}
private static class ConcreteParameterizedController extends AbstractParameterizedController<SimpleBean> {
}
private interface Identifiable extends Serializable {
Long getId();
void setId(Long id);
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name;
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
void handle(@RequestBody Mono<String> monoString, String paramWithoutAnnotation) {}
}
......@@ -59,13 +59,13 @@ import static org.junit.Assert.assertEquals;
/**
* Unit tests for {@link ResponseBodyResultHandler}.
*
* Unit tests for {@link ResponseBodyResultHandler}.When adding a test also
* consider whether the logic under test is in a parent class, then see:
* <ul>
* <li>{@code MessageConverterResultHandlerTests},
* <li>{@code ContentNegotiatingResultHandlerSupportTests}
* </ul>
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/
......
......@@ -120,13 +120,13 @@ public class ResponseEntityResultHandlerTests {
ResolvableType type = responseEntity(String.class);
assertTrue(this.resultHandler.supports(handlerResult(value, type)));
type = classWithGenerics(Mono.class, responseEntity(String.class));
type = forClassWithGenerics(Mono.class, responseEntity(String.class));
assertTrue(this.resultHandler.supports(handlerResult(value, type)));
type = classWithGenerics(Single.class, responseEntity(String.class));
type = forClassWithGenerics(Single.class, responseEntity(String.class));
assertTrue(this.resultHandler.supports(handlerResult(value, type)));
type = classWithGenerics(CompletableFuture.class, responseEntity(String.class));
type = forClassWithGenerics(CompletableFuture.class, responseEntity(String.class));
assertTrue(this.resultHandler.supports(handlerResult(value, type)));
type = ResolvableType.forClass(String.class);
......@@ -195,11 +195,7 @@ public class ResponseEntityResultHandlerTests {
private ResolvableType responseEntity(Class<?> bodyType) {
return classWithGenerics(ResponseEntity.class, ResolvableType.forClass(bodyType));
}
private ResolvableType classWithGenerics(Class<?> sourceType, ResolvableType genericType) {
return ResolvableType.forClassWithGenerics(sourceType, genericType);
return forClassWithGenerics(ResponseEntity.class, ResolvableType.forClass(bodyType));
}
private HandlerResult handlerResult(Object returnValue, ResolvableType type) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册