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) {