From 1e1e2f8b1ba4c8c9e734b2000788eb964c1c8ae7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jul 2016 04:16:04 -0400 Subject: [PATCH] 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>. --- ...tractMessageConverterArgumentResolver.java | 185 ++++++++ .../HttpEntityArgumentResolver.java | 107 +++++ .../RequestBodyArgumentResolver.java | 141 +----- .../HttpEntityArgumentResolverTests.java | 233 ++++++++++ ...MessageConverterArgumentResolverTests.java | 423 ++++++++++++++++++ .../RequestBodyArgumentResolverTests.java | 393 +--------------- .../ResponseBodyResultHandlerTests.java | 4 +- .../ResponseEntityResultHandlerTests.java | 12 +- 8 files changed, 980 insertions(+), 518 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java 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 0000000000..44f462d658 --- /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 0000000000..b2bb395e1d --- /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 d99240b436..8d8e7f01ac 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 0000000000..f42dce42ee --- /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 0000000000..ea6d7cd81c --- /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 471f11dbd8..a694d9c98e 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 06efa14061..cfd747d5a3 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 546c1bd0b4..93860f2a16 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) { -- GitLab