Rossen Stoyanchev
8 years ago
3 changed files with 284 additions and 3 deletions
@ -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<Object> 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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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<BindingResult> 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<Foo> fooMono, |
||||||
|
BindingResult bindingResult, |
||||||
|
Mono<Errors> errorsMono, |
||||||
|
String string) {} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue