/*
* Copyright 2002-2017 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.URISyntaxException;
import java.time.Duration;
import java.util.Map;
import java.util.function.Function;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import rx.RxReactiveStreams;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.method.ResolvableMethod;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.server.ServerWebExchange;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link ModelAttributeMethodArgumentResolver}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
*/
public class ModelAttributeMethodArgumentResolverTests {
private BindingContext bindContext;
private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build();
@Before
public void setup() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setValidator(validator);
this.bindContext = new BindingContext(initializer);
}
@Test
public void supports() throws Exception {
ModelAttributeMethodArgumentResolver resolver =
new ModelAttributeMethodArgumentResolver(new ReactiveAdapterRegistry(), false);
MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class);
assertTrue(resolver.supportsParameter(param));
param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
assertTrue(resolver.supportsParameter(param));
param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class);
assertFalse(resolver.supportsParameter(param));
param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
assertFalse(resolver.supportsParameter(param));
}
@Test
public void supportsWithDefaultResolution() throws Exception {
ModelAttributeMethodArgumentResolver resolver =
new ModelAttributeMethodArgumentResolver(new ReactiveAdapterRegistry(), true);
MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class);
assertTrue(resolver.supportsParameter(param));
param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
assertTrue(resolver.supportsParameter(param));
param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class);
assertFalse(resolver.supportsParameter(param));
param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, String.class);
assertFalse(resolver.supportsParameter(param));
}
@Test
public void createAndBind() throws Exception {
testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> {
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
}
@Test
public void createAndBindToMono() throws Exception {
MethodParameter parameter = this.testMethod
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
testBindFoo("fooMono", parameter, mono -> {
assertTrue(mono.getClass().getName(), mono instanceof Mono);
Object value = ((Mono<?>) mono).block(Duration.ofSeconds(5));
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
}
@Test
public void createAndBindToSingle() throws Exception {
MethodParameter parameter = this.testMethod
.annotPresent(ModelAttribute.class).arg(Single.class, Foo.class);
testBindFoo("fooSingle", parameter, single -> {
assertTrue(single.getClass().getName(), single instanceof Single);
Object value = ((Single<?>) single).toBlocking().value();
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
}
@Test
public void bindExisting() throws Exception {
Foo foo = new Foo();
foo.setName("Jim");
this.bindContext.getModel().addAttribute(foo);
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class);
testBindFoo("foo", parameter, value -> {
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
assertSame(foo, this.bindContext.getModel().asMap().get("foo"));
}
@Test
public void bindExistingMono() throws Exception {
Foo foo = new Foo();
foo.setName("Jim");
this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo));
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class);
testBindFoo("foo", parameter, value -> {
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
assertSame(foo, this.bindContext.getModel().asMap().get("foo"));
}
@Test
public void bindExistingSingle() throws Exception {
Foo foo = new Foo();
foo.setName("Jim");
this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo));
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class);
testBindFoo("foo", parameter, value -> {
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
assertSame(foo, this.bindContext.getModel().asMap().get("foo"));
}
@Test
public void bindExistingMonoToMono() throws Exception {
Foo foo = new Foo();
foo.setName("Jim");
String modelKey = "fooMono";
this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo));
MethodParameter parameter = this.testMethod
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
testBindFoo(modelKey, parameter, mono -> {
assertTrue(mono.getClass().getName(), mono instanceof Mono);
Object value = ((Mono<?>) mono).block(Duration.ofSeconds(5));
assertEquals(Foo.class, value.getClass());
return (Foo) value;
});
}
private void testBindFoo(String modelKey, MethodParameter param, Function<Object, Foo> valueExtractor)
throws Exception {
Object value = createResolver()
.resolveArgument(param, this.bindContext, postForm("name=Robert&age=25"))
.block(Duration.ZERO);
Foo foo = valueExtractor.apply(value);
assertEquals("Robert", foo.getName());
assertEquals(25, foo.getAge());
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey;
Map<String, Object> map = bindContext.getModel().asMap();
assertEquals(map.toString(), 2, map.size());
assertSame(foo, map.get(modelKey));
assertNotNull(map.get(bindingResultKey));
assertTrue(map.get(bindingResultKey) instanceof BindingResult);
}
@Test
public void validationError() throws Exception {
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class);
testValidationError(parameter, Function.identity());
}
@Test
@SuppressWarnings("unchecked")
public void validationErrorToMono() throws Exception {
MethodParameter parameter = this.testMethod
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
testValidationError(parameter,
resolvedArgumentMono -> {
Object value = resolvedArgumentMono.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue(value instanceof Mono);
return (Mono<?>) value;
});
}
@Test
public void validationErrorToSingle() throws Exception {
MethodParameter parameter = this.testMethod
.annotPresent(ModelAttribute.class).arg(Single.class, Foo.class);
testValidationError(parameter,
resolvedArgumentMono -> {
Object value = resolvedArgumentMono.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue(value instanceof Single);
return Mono.from(RxReactiveStreams.toPublisher((Single<?>) value));
});
}
private void testValidationError(MethodParameter param, Function<Mono<?>, Mono<?>> valueMonoExtractor)
throws URISyntaxException {
ServerWebExchange exchange = postForm("age=invalid");
Mono<?> mono = createResolver().resolveArgument(param, this.bindContext, exchange);
mono = valueMonoExtractor.apply(mono);
StepVerifier.create(mono)
.consumeErrorWith(ex -> {
assertTrue(ex instanceof WebExchangeBindException);
WebExchangeBindException bindException = (WebExchangeBindException) ex;
assertEquals(1, bindException.getErrorCount());
assertTrue(bindException.hasFieldErrors("age"));
})
.verify();
}
@Test
public void bindDataClass() throws Exception {
testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class));
}
private void testBindBar(MethodParameter param) throws Exception {
Object value = createResolver()
.resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1"))
.block(Duration.ZERO);
Bar bar = (Bar) value;
assertEquals("Robert", bar.getName());
assertEquals(25, bar.getAge());
assertEquals(1, bar.getCount());
String key = "bar";
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
Map<String, Object> map = bindContext.getModel().asMap();
assertEquals(map.toString(), 2, map.size());
assertSame(bar, map.get(key));
assertNotNull(map.get(bindingResultKey));
assertTrue(map.get(bindingResultKey) instanceof BindingResult);
}
private ModelAttributeMethodArgumentResolver createResolver() {
return new ModelAttributeMethodArgumentResolver(new ReactiveAdapterRegistry(), false);
}
private ServerWebExchange postForm(String formData) throws URISyntaxException {
return MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(formData)
.toExchange();
}
@SuppressWarnings("unused")
void handle(
@ModelAttribute @Validated Foo foo,
@ModelAttribute @Validated Mono<Foo> mono,
@ModelAttribute @Validated Single<Foo> single,
Foo fooNotAnnotated,
String stringNotAnnotated,
Mono<Foo> monoNotAnnotated,
Mono<String> monoStringNotAnnotated,
Bar barNotAnnotated) {
}
@SuppressWarnings("unused")
private static class Foo {
private String name;
private int age;
public Foo() {
}
public Foo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
private static class Bar {
private final String name;
private final int age;
private int count;
public Bar(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return this.age;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
}