Rossen Stoyanchev
8 years ago
9 changed files with 506 additions and 80 deletions
@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
|
||||
/* |
||||
* 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.reflect.Method; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.Stream; |
||||
|
||||
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.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.bind.annotation.ModelAttribute; |
||||
import org.springframework.web.bind.support.WebBindingInitializer; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
import org.springframework.web.reactive.HandlerResult; |
||||
import org.springframework.web.reactive.result.method.BindingContext; |
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; |
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod; |
||||
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; |
||||
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* A helper class for {@link RequestMappingHandlerAdapter} that assists with |
||||
* creating a {@code BindingContext} and initialize it, and its model, through |
||||
* {@code @InitBinder} and {@code @ModelAttribute} methods. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
class BindingContextFactory { |
||||
|
||||
private final RequestMappingHandlerAdapter adapter; |
||||
|
||||
|
||||
public BindingContextFactory(RequestMappingHandlerAdapter adapter) { |
||||
this.adapter = adapter; |
||||
} |
||||
|
||||
|
||||
public RequestMappingHandlerAdapter getAdapter() { |
||||
return this.adapter; |
||||
} |
||||
|
||||
private WebBindingInitializer getBindingInitializer() { |
||||
return getAdapter().getWebBindingInitializer(); |
||||
} |
||||
|
||||
private List<SyncHandlerMethodArgumentResolver> getInitBinderArgumentResolvers() { |
||||
return getAdapter().getInitBinderArgumentResolvers(); |
||||
} |
||||
|
||||
private List<HandlerMethodArgumentResolver> getArgumentResolvers() { |
||||
return getAdapter().getArgumentResolvers(); |
||||
} |
||||
|
||||
private ReactiveAdapterRegistry getAdapterRegistry() { |
||||
return getAdapter().getReactiveAdapterRegistry(); |
||||
} |
||||
|
||||
private Stream<Method> getInitBinderMethods(HandlerMethod handlerMethod) { |
||||
return getAdapter().getInitBinderMethods(handlerMethod.getBeanType()).stream(); |
||||
} |
||||
|
||||
private Stream<Method> getModelAttributeMethods(HandlerMethod handlerMethod) { |
||||
return getAdapter().getModelAttributeMethods(handlerMethod.getBeanType()).stream(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Create and initialize a BindingContext for the current request. |
||||
* @param handlerMethod the request handling method |
||||
* @param exchange the current exchange |
||||
* @return Mono with the BindingContext instance |
||||
*/ |
||||
public Mono<BindingContext> createBindingContext(HandlerMethod handlerMethod, |
||||
ServerWebExchange exchange) { |
||||
|
||||
List<SyncInvocableHandlerMethod> invocableMethods = getInitBinderMethods(handlerMethod) |
||||
.map(method -> { |
||||
Object bean = handlerMethod.getBean(); |
||||
SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method); |
||||
invocable.setSyncArgumentResolvers(getInitBinderArgumentResolvers()); |
||||
return invocable; |
||||
}) |
||||
.collect(Collectors.toList()); |
||||
|
||||
BindingContext bindingContext = |
||||
new InitBinderBindingContext(getBindingInitializer(), invocableMethods); |
||||
|
||||
return initModel(handlerMethod, bindingContext, exchange).then(Mono.just(bindingContext)); |
||||
} |
||||
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
private Mono<Void> initModel(HandlerMethod handlerMethod, BindingContext context, |
||||
ServerWebExchange exchange) { |
||||
|
||||
List<Mono<HandlerResult>> resultMonos = getModelAttributeMethods(handlerMethod) |
||||
.map(method -> { |
||||
Object bean = handlerMethod.getBean(); |
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method); |
||||
invocable.setArgumentResolvers(getArgumentResolvers()); |
||||
return invocable; |
||||
}) |
||||
.map(invocable -> invocable.invoke(exchange, context)) |
||||
.collect(Collectors.toList()); |
||||
|
||||
return Mono |
||||
.when(resultMonos, resultArr -> processModelMethodMonos(resultArr, context)) |
||||
.then(voidMonos -> Mono.when(voidMonos)); |
||||
} |
||||
|
||||
private List<Mono<Void>> processModelMethodMonos(Object[] resultArr, BindingContext context) { |
||||
return Arrays.stream(resultArr) |
||||
.map(result -> processModelMethodResult((HandlerResult) result, context)) |
||||
.collect(Collectors.toList()); |
||||
} |
||||
|
||||
private Mono<Void> processModelMethodResult(HandlerResult result, BindingContext context) { |
||||
Object value = result.getReturnValue().orElse(null); |
||||
if (value == null) { |
||||
return Mono.empty(); |
||||
} |
||||
|
||||
ResolvableType type = result.getReturnType(); |
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(type.getRawClass(), value); |
||||
Class<?> valueType = (adapter != null ? type.resolveGeneric(0) : type.resolve()); |
||||
|
||||
if (Void.class.equals(valueType) || void.class.equals(valueType)) { |
||||
return (adapter != null ? adapter.toMono(value) : Mono.empty()); |
||||
} |
||||
|
||||
String name = getAttributeName(valueType, result.getReturnTypeSource()); |
||||
context.getModel().asMap().putIfAbsent(name, value); |
||||
return Mono.empty(); |
||||
} |
||||
|
||||
private String getAttributeName(Class<?> valueType, MethodParameter parameter) { |
||||
Method method = parameter.getMethod(); |
||||
ModelAttribute annot = AnnotatedElementUtils.findMergedAnnotation(method, ModelAttribute.class); |
||||
if (annot != null && StringUtils.hasText(annot.value())) { |
||||
return annot.value(); |
||||
} |
||||
// TODO: Conventions does not deal with async wrappers
|
||||
return ClassUtils.getShortNameAsProperty(valueType); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,182 @@
@@ -0,0 +1,182 @@
|
||||
/* |
||||
* 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.Collections; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
import rx.Single; |
||||
|
||||
import org.springframework.context.support.StaticApplicationContext; |
||||
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.ui.Model; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.validation.Validator; |
||||
import org.springframework.web.bind.WebDataBinder; |
||||
import org.springframework.web.bind.WebExchangeDataBinder; |
||||
import org.springframework.web.bind.annotation.InitBinder; |
||||
import org.springframework.web.bind.annotation.ModelAttribute; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
import org.springframework.web.reactive.config.WebReactiveConfigurationSupport; |
||||
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.DefaultWebSessionManager; |
||||
import org.springframework.web.server.session.WebSessionManager; |
||||
|
||||
import static org.junit.Assert.assertArrayEquals; |
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Unit tests for {@link BindingContextFactory}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class BindingContextFactoryTests { |
||||
|
||||
private BindingContextFactory contextFactory; |
||||
|
||||
private ServerWebExchange exchange; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
WebReactiveConfigurationSupport configurationSupport = new WebReactiveConfigurationSupport(); |
||||
configurationSupport.setApplicationContext(new StaticApplicationContext()); |
||||
RequestMappingHandlerAdapter adapter = configurationSupport.requestMappingHandlerAdapter(); |
||||
adapter.afterPropertiesSet(); |
||||
this.contextFactory = new BindingContextFactory(adapter); |
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/path"); |
||||
MockServerHttpResponse response = new MockServerHttpResponse(); |
||||
WebSessionManager manager = new DefaultWebSessionManager(); |
||||
this.exchange = new DefaultServerWebExchange(request, response, manager); |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings("unchecked") |
||||
@Test |
||||
public void basic() throws Exception { |
||||
|
||||
Validator validator = mock(Validator.class); |
||||
TestController controller = new TestController(validator); |
||||
|
||||
HandlerMethod handlerMethod = ResolvableMethod.on(controller) |
||||
.annotated(RequestMapping.class) |
||||
.resolveHandlerMethod(); |
||||
|
||||
BindingContext bindingContext = |
||||
this.contextFactory.createBindingContext(handlerMethod, this.exchange) |
||||
.blockMillis(5000); |
||||
|
||||
WebExchangeDataBinder binder = bindingContext.createDataBinder(this.exchange, "name"); |
||||
assertEquals(Collections.singletonList(validator), binder.getValidators()); |
||||
|
||||
Map<String, Object> model = bindingContext.getModel().asMap(); |
||||
assertEquals(5, model.size()); |
||||
|
||||
Object value = model.get("bean"); |
||||
assertEquals("Bean", ((TestBean) value).getName()); |
||||
|
||||
value = model.get("monoBean"); |
||||
assertEquals("Mono Bean", ((Mono<TestBean>) value).blockMillis(5000).getName()); |
||||
|
||||
value = model.get("singleBean"); |
||||
assertEquals("Single Bean", ((Single<TestBean>) value).toBlocking().value().getName()); |
||||
|
||||
value = model.get("voidMethodBean"); |
||||
assertEquals("Void Method Bean", ((TestBean) value).getName()); |
||||
|
||||
value = model.get("voidMonoMethodBean"); |
||||
assertEquals("Void Mono Method Bean", ((TestBean) value).getName()); |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings("unused") |
||||
private static class TestController { |
||||
|
||||
private Validator[] validators; |
||||
|
||||
|
||||
public TestController(Validator... validators) { |
||||
this.validators = validators; |
||||
} |
||||
|
||||
|
||||
@InitBinder |
||||
public void initDataBinder(WebDataBinder dataBinder) { |
||||
if (!ObjectUtils.isEmpty(this.validators)) { |
||||
dataBinder.addValidators(this.validators); |
||||
} |
||||
} |
||||
|
||||
@ModelAttribute("bean") |
||||
public TestBean returnValue() { |
||||
return new TestBean("Bean"); |
||||
} |
||||
|
||||
@ModelAttribute("monoBean") |
||||
public Mono<TestBean> returnValueMono() { |
||||
return Mono.just(new TestBean("Mono Bean")); |
||||
} |
||||
|
||||
@ModelAttribute("singleBean") |
||||
public Single<TestBean> returnValueSingle() { |
||||
return Single.just(new TestBean("Single Bean")); |
||||
} |
||||
|
||||
@ModelAttribute |
||||
public void voidMethodBean(Model model) { |
||||
model.addAttribute("voidMethodBean", new TestBean("Void Method Bean")); |
||||
} |
||||
|
||||
@ModelAttribute |
||||
public Mono<Void> voidMonoMethodBean(Model model) { |
||||
return Mono.just("Void Mono Method Bean") |
||||
.doOnNext(name -> model.addAttribute("voidMonoMethodBean", new TestBean(name))) |
||||
.then(); |
||||
} |
||||
|
||||
@RequestMapping |
||||
public void handle() {} |
||||
} |
||||
|
||||
private static class TestBean { |
||||
|
||||
private final String name; |
||||
|
||||
TestBean(String name) { |
||||
this.name = name; |
||||
} |
||||
|
||||
public String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "TestBean[name=" + this.name + "]"; |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue