diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java
new file mode 100644
index 00000000000..44f462d6587
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java
@@ -0,0 +1,185 @@
+/*
+ * 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}.
+ *
+ *
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> messageConverters;
+
+ private final ConversionService conversionService;
+
+ private final Validator validator;
+
+ private final List 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> 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> getMessageConverters() {
+ return this.messageConverters;
+ }
+
+ /**
+ * Return the configured {@link ConversionService}.
+ */
+ public ConversionService getConversionService() {
+ return this.conversionService;
+ }
+
+
+ protected Mono 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 Function 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);
+ }
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java
new file mode 100644
index 00000000000..b2bb395e1d1
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java
@@ -0,0 +1,107 @@
+/*
+ * 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> 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> 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 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);
+ }
+ });
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
index d99240b436f..8d8e7f01acd 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
@@ -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}.
+ *
+ * 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> messageConverters;
-
- private final ConversionService conversionService;
-
- private final Validator validator;
-
- private final List supportedMediaTypes;
+public class RequestBodyArgumentResolver extends AbstractMessageConverterArgumentResolver
+ implements HandlerMethodArgumentResolver {
/**
@@ -92,29 +68,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
public RequestBodyArgumentResolver(List> 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> 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 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 Function 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 resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) {
+ return readBody(param, exchange);
}
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
new file mode 100644
index 00000000000..f42dce42eec
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
@@ -0,0 +1,233 @@
+/*
+ * 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 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> 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> 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> 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> 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 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> 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 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 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 monoString,
+ HttpEntity httpEntity,
+ HttpEntity> monoBody,
+ HttpEntity> singleBody,
+ HttpEntity> completableFutureBody,
+ HttpEntity> fluxBody,
+ HttpEntity> observableBody,
+ RequestEntity requestEntity) {}
+
+}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java
new file mode 100644
index 00000000000..ea6d7cd81cb
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java
@@ -0,0 +1,423 @@
+/*
+ * 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 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 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 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 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 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 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 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 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 resolveValue(MethodParameter param, String body) {
+
+ this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+ this.request.writeWith(Flux.just(dataBuffer(body)));
+
+ Mono 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> 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 monoTestBean,
+ @Validated Flux fluxTestBean,
+ Single singleTestBean,
+ Observable observableTestBean,
+ CompletableFuture futureTestBean,
+ TestBean testBean,
+ Map map,
+ List list,
+ Mono> monoList,
+ Set 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 {
+
+ @SuppressWarnings("unused")
+ public void handleDto(DTO dto) {}
+ }
+
+ private static class ConcreteParameterizedController extends AbstractParameterizedController {
+ }
+
+ 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;
+ }
+ }
+
+}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
index 471f11dbd84..a694d9c98e6 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
@@ -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 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 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 mono = (Mono) 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 flux = (Flux) 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 single = (Single) 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 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 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 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 resolveValue(MethodParameter param, Class valueType, String body) {
-
- this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
- this.request.writeWith(Flux.just(dataBuffer(body)));
-
- Mono 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> 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 monoTestBean,
- @Validated @RequestBody Flux fluxTestBean,
- @RequestBody Single singleTestBean,
- @RequestBody Observable observableTestBean,
- @RequestBody CompletableFuture futureTestBean,
- @RequestBody TestBean testBean,
- @RequestBody Map map,
- @RequestBody List list,
- @RequestBody Mono> monoList,
- @RequestBody Set 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 {
-
- @SuppressWarnings("unused")
- public void handleDto(@RequestBody DTO dto) {}
- }
-
- private static class ConcreteParameterizedController extends AbstractParameterizedController {
- }
-
- 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 monoString, String paramWithoutAnnotation) {}
}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java
index 06efa140612..cfd747d5a31 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java
@@ -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:
*
* {@code MessageConverterResultHandlerTests},
* {@code ContentNegotiatingResultHandlerSupportTests}
*
+ *
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
index 546c1bd0b43..93860f2a164 100644
--- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
@@ -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) {