From d163240ed45f1e1f37da8a1245dab4c4d2866d54 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Nov 2016 08:19:14 +0200 Subject: [PATCH] Reactive support for Errors argument Issue: SPR-14542 --- .../ErrorsMethodArgumentResolver.java | 118 +++++++++++++ .../ModelAttributeMethodArgumentResolver.java | 8 +- .../ErrorsArgumentResolverTests.java | 161 ++++++++++++++++++ 3 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsArgumentResolverTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java new file mode 100644 index 0000000000..8bba67f918 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java @@ -0,0 +1,118 @@ +/* + * 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 reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.reactive.result.method.BindingContext; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolve {@link Errors} or {@link BindingResult} method arguments. + * An {@code Errors} argument is expected to appear immediately after the + * model attribute in the method signature. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ErrorsMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final ReactiveAdapterRegistry adapterRegistry; + + + /** + * Class constructor. + * @param registry for adapting to other reactive types from and to Mono + */ + public ErrorsMethodArgumentResolver(ReactiveAdapterRegistry registry) { + Assert.notNull(registry, "'ReactiveAdapterRegistry' is required."); + this.adapterRegistry = registry; + } + + + /** + * Return the configured {@link ReactiveAdapterRegistry}. + */ + public ReactiveAdapterRegistry getAdapterRegistry() { + return this.adapterRegistry; + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class clazz = parameter.getParameterType(); + return Errors.class.isAssignableFrom(clazz); + } + + + @Override + public Mono resolveArgument(MethodParameter parameter, BindingContext context, + ServerWebExchange exchange) { + + String name = getModelAttributeName(parameter); + Object errors = context.getModel().asMap().get(BindingResult.MODEL_KEY_PREFIX + name); + + Mono errorsMono; + if (Mono.class.isAssignableFrom(errors.getClass())) { + errorsMono = (Mono) errors; + } + else if (Errors.class.isAssignableFrom(errors.getClass())) { + errorsMono = Mono.just(errors); + } + else { + throw new IllegalStateException( + "Unexpected Errors/BindingResult type: " + errors.getClass().getName()); + } + + return errorsMono.cast(Object.class); + } + + private String getModelAttributeName(MethodParameter parameter) { + + Assert.isTrue(parameter.getParameterIndex() > 0, + "Errors argument must be immediately after a model attribute argument."); + + int index = parameter.getParameterIndex() - 1; + MethodParameter attributeParam = new MethodParameter(parameter.getMethod(), index); + Class attributeType = attributeParam.getParameterType(); + + ResolvableType type = ResolvableType.forMethodParameter(attributeParam); + ReactiveAdapter adapterTo = getAdapterRegistry().getAdapterTo(type.resolve()); + + Assert.isNull(adapterTo, "Errors/BindingResult cannot be used with an async model attribute. " + + "Either declare the model attribute without the async wrapper type " + + "or handle WebExchangeBindException through the async type."); + + ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class); + if (annot != null && StringUtils.hasText(annot.value())) { + return annot.value(); + } + // TODO: Conventions does not deal with async wrappers + return ClassUtils.getShortNameAsProperty(attributeType); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 2598937f17..702f26bd28 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -152,10 +152,12 @@ public class ModelAttributeMethodArgumentResolver implements HandlerMethodArgume } private String getAttributeName(Class valueType, MethodParameter parameter) { - ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); - String name = (ann != null ? ann.value() : null); + ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class); + if (annot != null && StringUtils.hasText(annot.value())) { + return annot.value(); + } // TODO: Conventions does not deal with async wrappers - return StringUtils.hasText(name) ? name : ClassUtils.getShortNameAsProperty(valueType); + return ClassUtils.getShortNameAsProperty(valueType); } private Mono getAttributeMono(String attributeName, Class attributeType, diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsArgumentResolverTests.java new file mode 100644 index 0000000000..b4b414ec3c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsArgumentResolverTests.java @@ -0,0 +1,161 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpMethod; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.WebExchangeDataBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.reactive.result.ResolvableMethod; +import org.springframework.web.reactive.result.method.BindingContext; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.core.ResolvableType.forClassWithGenerics; + +/** + * Unit tests for {@link ErrorsMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class ErrorsArgumentResolverTests { + + private ErrorsMethodArgumentResolver resolver ; + + private final BindingContext bindingContext = new BindingContext(); + + private BindingResult bindingResult; + + private ServerWebExchange exchange; + + private final ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle"); + + + @Before + public void setUp() throws Exception { + this.resolver = new ErrorsMethodArgumentResolver(new ReactiveAdapterRegistry()); + + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "/path"); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new MockWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, response, manager); + + Foo foo = new Foo(); + WebExchangeDataBinder binder = this.bindingContext.createDataBinder(this.exchange, foo, "foo"); + this.bindingResult = binder.getBindingResult(); + } + + + @Test + public void supports() throws Exception { + + MethodParameter parameter = parameter(forClass(Errors.class)); + assertTrue(this.resolver.supportsParameter(parameter)); + + parameter = parameter(forClass(BindingResult.class)); + assertTrue(this.resolver.supportsParameter(parameter)); + + parameter = parameter(forClassWithGenerics(Mono.class, Errors.class)); + assertFalse(this.resolver.supportsParameter(parameter)); + + parameter = parameter(forClass(String.class)); + assertFalse(this.resolver.supportsParameter(parameter)); + } + + @Test + public void resolveErrors() throws Exception { + testResolve(this.bindingResult); + } + + @Test + public void resolveErrorsMono() throws Exception { + MonoProcessor monoProcessor = MonoProcessor.create(); + monoProcessor.onNext(this.bindingResult); + testResolve(monoProcessor); + } + + @Test(expected = IllegalArgumentException.class) + public void resolveErrorsAfterMonoModelAttribute() throws Exception { + MethodParameter parameter = parameter(forClass(BindingResult.class)); + this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange).blockMillis(5000); + } + + + private void testResolve(Object bindingResult) { + + String key = BindingResult.MODEL_KEY_PREFIX + "foo"; + this.bindingContext.getModel().asMap().put(key, bindingResult); + + MethodParameter parameter = parameter(forClass(Errors.class)); + + Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) + .blockMillis(5000); + + assertSame(this.bindingResult, actual); + } + + + private MethodParameter parameter(ResolvableType type) { + return this.testMethod.resolveParam(type); + } + + + private static class Foo { + + private String name; + + public Foo() { + } + + public Foo(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @SuppressWarnings("unused") + void handle( + @ModelAttribute Foo foo, + Errors errors, + @ModelAttribute Mono fooMono, + BindingResult bindingResult, + Mono errorsMono, + String string) {} + +}