diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java index fc8ee41cff..f699df1e25 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java @@ -15,11 +15,7 @@ */ package org.springframework.web.reactive.result.method; -import java.lang.reflect.Field; - import org.springframework.beans.TypeConverter; -import org.springframework.beans.TypeMismatchException; -import org.springframework.core.MethodParameter; import org.springframework.ui.ModelMap; import org.springframework.validation.support.BindingAwareModelMap; import org.springframework.web.bind.WebDataBinder; @@ -27,6 +23,7 @@ import org.springframework.web.bind.WebExchangeDataBinder; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.server.ServerWebExchange; + /** * A context for binding requests to method arguments that provides access to * the default model, data binding, validation, and type conversion. @@ -34,7 +31,7 @@ import org.springframework.web.server.ServerWebExchange; * @author Rossen Stoyanchev * @since 5.0 */ -public class BindingContext implements TypeConverter { +public class BindingContext { private final ModelMap model = new BindingAwareModelMap(); @@ -68,46 +65,50 @@ public class BindingContext implements TypeConverter { return this.model; } - /** - * Create a {@link WebExchangeDataBinder} for the given object. - * @param exchange the current exchange - * @param target the object to create a data binder for, or {@code null} if - * creating a binder for a simple type - * @param objectName the name of the target object - * @return a Mono for the created {@link WebDataBinder} instance - */ - public WebExchangeDataBinder createBinder(ServerWebExchange exchange, Object target, - String objectName) { - WebExchangeDataBinder dataBinder = createBinderInstance(target, objectName); + /** + * Create a {@link WebExchangeDataBinder} for applying data binding, type + * conversion, and validation on the given "target" object. + * + * @param exchange the current exchange + * @param target the object to create a data binder for + * @param name the name of the target object + * @return the {@link WebDataBinder} instance + */ + public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, Object target, String name) { + WebExchangeDataBinder dataBinder = createBinderInstance(target, name); if (this.initializer != null) { this.initializer.initBinder(dataBinder); } - return initBinder(dataBinder, exchange); + return initDataBinder(dataBinder, exchange); } + /** + * Create a {@link WebExchangeDataBinder} without a "target" object, i.e. + * for applying type conversion to simple types. + * + * @param exchange the current exchange + * @param name the name of the target object + * @return a Mono for the created {@link WebDataBinder} instance + */ + public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String name) { + return createDataBinder(exchange, null, name); + } + + /** + * Create the data binder instance. + */ protected WebExchangeDataBinder createBinderInstance(Object target, String objectName) { return new WebExchangeDataBinder(target, objectName); } - protected WebExchangeDataBinder initBinder(WebExchangeDataBinder binder, ServerWebExchange exchange) { + /** + * Initialize the data binder instance for the given exchange. + */ + protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, + ServerWebExchange exchange) { + return binder; } - public T convertIfNecessary(Object value, Class requiredType) throws TypeMismatchException { - return this.simpleValueTypeConverter.convertIfNecessary(value, requiredType); - } - - public T convertIfNecessary(Object value, Class requiredType, MethodParameter methodParam) - throws TypeMismatchException { - - return this.simpleValueTypeConverter.convertIfNecessary(value, requiredType, methodParam); - } - - public T convertIfNecessary(Object value, Class requiredType, Field field) - throws TypeMismatchException { - - return this.simpleValueTypeConverter.convertIfNecessary(value, requiredType, field); - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 202264fb5b..a8dacdbc8a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -67,11 +67,24 @@ public class InvocableHandlerMethod extends HandlerMethod { } - public void setHandlerMethodArgumentResolvers(List resolvers) { + /** + * Set {@link HandlerMethodArgumentResolver}s to use to use for resolving + * method argument values. + */ + public void setArgumentResolvers(List resolvers) { this.resolvers.clear(); this.resolvers.addAll(resolvers); } + /** + * Set the ParameterNameDiscoverer for resolving parameter names when needed + * (e.g. default request attribute name). + *

Default is a {@link DefaultParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + @Override protected Method getBridgedMethod() { return super.getBridgedMethod(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/SyncInvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/SyncInvocableHandlerMethod.java index c82ae7486d..8976ca1413 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/SyncInvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/SyncInvocableHandlerMethod.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.result.method; import java.lang.reflect.Method; +import java.util.Collections; import java.util.List; import org.springframework.util.Assert; @@ -48,11 +49,11 @@ public class SyncInvocableHandlerMethod extends InvocableHandlerMethod { * all resolvers are {@link SyncHandlerMethodArgumentResolver}. */ @Override - public void setHandlerMethodArgumentResolvers(List resolvers) { + public void setArgumentResolvers(List resolvers) { resolvers.forEach(resolver -> Assert.isInstanceOf(SyncHandlerMethodArgumentResolver.class, resolver, "Expected sync argument resolver: " + resolver.getClass().getName())); - super.setHandlerMethodArgumentResolvers(resolvers); + super.setArgumentResolvers(resolvers); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index 43e54cddaf..5bb4b236a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -213,7 +213,7 @@ public abstract class AbstractMessageReaderArgumentResolver { MethodParameter param, BindingContext binding, ServerWebExchange exchange) { String name = Conventions.getVariableNameForParameter(param); - WebExchangeDataBinder binder = binding.createBinder(exchange, target, name); + WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name); binder.validate(validationHints); if (binder.getBindingResult().hasErrors()) { throw new ServerWebInputException("Validation failed", param); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java index 50577e8639..30a370d654 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.config.BeanExpressionResolver; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.ui.ModelMap; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.reactive.result.method.BindingContext; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; @@ -93,7 +94,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } - arg = applyConversion(arg, parameter, bindingContext); + arg = applyConversion(arg, namedValueInfo, parameter, bindingContext, exchange); handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange); return arg; }) @@ -168,9 +169,12 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho protected abstract Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange); - private Object applyConversion(Object value, MethodParameter parameter, BindingContext bindingContext) { + private Object applyConversion(Object value, NamedValueInfo namedValueInfo, MethodParameter parameter, + BindingContext bindingContext, ServerWebExchange exchange) { + + WebDataBinder binder = bindingContext.createDataBinder(exchange, namedValueInfo.name); try { - value = bindingContext.convertIfNecessary(value, parameter.getParameterType(), parameter); + value = binder.convertIfNecessary(value, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new ServerErrorException("Conversion not supported.", parameter, ex); @@ -193,7 +197,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho handleMissingValue(namedValueInfo.name, parameter, exchange); } value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType()); - value = applyConversion(value, parameter, bindingContext); + value = applyConversion(value, namedValueInfo, parameter, bindingContext, exchange); handleResolvedValue(value, namedValueInfo.name, parameter, model, exchange); return Mono.justOrEmpty(value); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java new file mode 100644 index 0000000000..d51f3ca6cf --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java @@ -0,0 +1,87 @@ +/* + * 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.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.WebExchangeDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +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.SyncInvocableHandlerMethod; +import org.springframework.web.server.ServerWebExchange; + +/** + * An extension of {@link BindingContext} that uses {@code @InitBinder} methods + * to initialize a data binder instance. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class InitBinderBindingContext extends BindingContext { + + private final List binderMethods; + + /** BindingContext for @InitBinder method invocation */ + private final BindingContext bindingContext; + + + public InitBinderBindingContext(WebBindingInitializer initializer, + List binderMethods) { + + super(initializer); + this.binderMethods = binderMethods; + this.bindingContext = new BindingContext(initializer); + } + + + @Override + protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, + ServerWebExchange exchange) { + + this.binderMethods.stream() + .filter(method -> isBinderMethodApplicable(method, binder)) + .forEach(method -> invokeInitBinderMethod(binder, exchange, method)); + + return binder; + } + + /** + * Whether the given {@code @InitBinder} method should be used to initialize + * the given WebDataBinder instance. By default we check the attributes + * names of the annotation, if present. + */ + protected boolean isBinderMethodApplicable(HandlerMethod binderMethod, WebDataBinder binder) { + InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class); + Collection names = Arrays.asList(annot.value()); + return (names.size() == 0 || names.contains(binder.getObjectName())); + } + + private void invokeInitBinderMethod(WebExchangeDataBinder binder, ServerWebExchange exchange, + SyncInvocableHandlerMethod method) { + + HandlerResult result = method.invokeForHandlerResult(exchange, this.bindingContext, binder); + if (result.getReturnValue().isPresent()) { + throw new IllegalStateException("@InitBinder methods should return void: " + method); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 572a931a60..55e8ae83b9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; @@ -31,12 +32,16 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodIntrospector; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.HttpMessageReader; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; @@ -45,6 +50,8 @@ 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; /** @@ -57,6 +64,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); + private final List> messageReaders = new ArrayList<>(10); private WebBindingInitializer webBindingInitializer; @@ -67,8 +75,15 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory private List argumentResolvers; + private List customInitBinderArgumentResolvers; + + private List initBinderArgumentResolvers; + private ConfigurableBeanFactory beanFactory; + + private final Map, Set> initBinderCache = new ConcurrentHashMap<>(64); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); @@ -119,10 +134,10 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory } /** - * Provide custom argument resolvers without overriding the built-in ones. + * Configure custom argument resolvers without overriding the built-in ones. */ - public void setCustomArgumentResolvers(List argumentResolvers) { - this.customArgumentResolvers = argumentResolvers; + public void setCustomArgumentResolvers(List resolvers) { + this.customArgumentResolvers = resolvers; } /** @@ -147,6 +162,39 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return this.argumentResolvers; } + /** + * Configure custom argument resolvers for {@code @InitBinder} methods. + */ + public void setCustomInitBinderArgumentResolvers(List resolvers) { + this.customInitBinderArgumentResolvers = resolvers; + } + + /** + * Return the custom {@code @InitBinder} argument resolvers. + */ + public List getCustomInitBinderArgumentResolvers() { + return this.customInitBinderArgumentResolvers; + } + + /** + * Configure the supported argument types in {@code @InitBinder} methods. + */ + public void setInitBinderArgumentResolvers(List resolvers) { + this.initBinderArgumentResolvers = null; + if (resolvers != null) { + this.initBinderArgumentResolvers = new ArrayList<>(); + this.initBinderArgumentResolvers.addAll(resolvers); + } + } + + /** + * Return the configured argument resolvers for {@code @InitBinder} methods. + */ + public List getInitBinderArgumentResolvers() { + return this.initBinderArgumentResolvers; + } + + /** * A {@link ConfigurableBeanFactory} is expected for resolving expressions * in method argument default values. @@ -166,21 +214,22 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory @Override public void afterPropertiesSet() throws Exception { if (this.argumentResolvers == null) { - this.argumentResolvers = initArgumentResolvers(); + this.argumentResolvers = getDefaultArgumentResolvers(); + } + if (this.initBinderArgumentResolvers == null) { + this.initBinderArgumentResolvers = getDefaultInitBinderArgumentResolvers(); } } - protected List initArgumentResolvers() { + protected List getDefaultArgumentResolvers() { List resolvers = new ArrayList<>(); - ReactiveAdapterRegistry adapterRegistry = getReactiveAdapterRegistry(); - // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory())); resolvers.add(new PathVariableMapMethodArgumentResolver()); - resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), adapterRegistry)); + resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry())); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory())); @@ -189,7 +238,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory())); // Type-based argument resolution - resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), adapterRegistry)); + resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry())); resolvers.add(new ModelArgumentResolver()); resolvers.add(new ServerWebExchangeArgumentResolver()); resolvers.add(new WebSessionArgumentResolver()); @@ -204,6 +253,35 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return resolvers; } + protected List getDefaultInitBinderArgumentResolvers() { + List resolvers = new ArrayList<>(); + + // Annotation-based argument resolution + resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); + resolvers.add(new RequestParamMapMethodArgumentResolver()); + resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory())); + resolvers.add(new PathVariableMapMethodArgumentResolver()); + resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); + resolvers.add(new RequestHeaderMapMethodArgumentResolver()); + resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory())); + resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); + resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory())); + + // Type-based argument resolution + resolvers.add(new ModelArgumentResolver()); + resolvers.add(new ServerWebExchangeArgumentResolver()); + + // Custom resolvers + if (getCustomArgumentResolvers() != null) { + resolvers.addAll(getCustomInitBinderArgumentResolvers()); + } + + // Catch-all + resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); + return resolvers; + } + + @Override public boolean supports(Object handler) { return HandlerMethod.class.equals(handler.getClass()); @@ -213,8 +291,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory public Mono handle(ServerWebExchange exchange, Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); - invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); - BindingContext bindingContext = new BindingContext(getWebBindingInitializer()); + invocable.setArgumentResolvers(getArgumentResolvers()); + BindingContext bindingContext = getBindingContext(handlerMethod); return invocable.invoke(exchange, bindingContext) .map(result -> result.setExceptionHandler( ex -> handleException(ex, handlerMethod, bindingContext, exchange))) @@ -222,6 +300,24 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory ex, handlerMethod, bindingContext, exchange)); } + private BindingContext getBindingContext(HandlerMethod handlerMethod) { + Class handlerType = handlerMethod.getBeanType(); + Set methods = this.initBinderCache.get(handlerType); + if (methods == null) { + methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS); + this.initBinderCache.put(handlerType, methods); + } + List initBinderMethods = new ArrayList<>(); + for (Method method : methods) { + Object bean = handlerMethod.getBean(); + SyncInvocableHandlerMethod initBinderMethod = new SyncInvocableHandlerMethod(bean, method); + initBinderMethod.setArgumentResolvers(new ArrayList<>(this.initBinderArgumentResolvers)); + initBinderMethods.add(initBinderMethod); + } + return new InitBinderBindingContext(getWebBindingInitializer(), initBinderMethods); + } + + private Mono handleException(Throwable ex, HandlerMethod handlerMethod, BindingContext bindingContext, ServerWebExchange exchange) { @@ -231,7 +327,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory if (logger.isDebugEnabled()) { logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod()); } - invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); + invocable.setArgumentResolvers(getArgumentResolvers()); bindingContext.getModel().clear(); return invocable.invoke(exchange, bindingContext, ex); } @@ -259,4 +355,11 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return (method != null ? new InvocableHandlerMethod(handlerMethod.getBean(), method) : null); } + + /** + * MethodFilter that matches {@link InitBinder @InitBinder} methods. + */ + public static final ReflectionUtils.MethodFilter INIT_BINDER_METHODS = + method -> AnnotationUtils.findAnnotation(method, InitBinder.class) != null; + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 91f817ef36..07bc87eac4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -154,7 +154,7 @@ public class InvocableHandlerMethodTests { HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); when(resolver.supportsParameter(any())).thenReturn(true); when(resolver.resolveArgument(any(), any(), any())).thenReturn(resolvedValue); - handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); + handlerMethod.setArgumentResolvers(Collections.singletonList(resolver)); } private void assertHandlerResultValue(Mono mono, String expected) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java new file mode 100644 index 0000000000..d66b1fdfd3 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -0,0 +1,163 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.reactive.result.method.BindingContext; +import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + + +/** + * Unit tests for {@link InitBinderBindingContext}. + * @author Rossen Stoyanchev + */ +public class InitBinderBindingContextTests { + + private final ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + + private final List argumentResolvers = new ArrayList<>(); + + private final ServerWebExchange exchange = new DefaultServerWebExchange( + new MockServerHttpRequest(), new MockServerHttpResponse(), new DefaultWebSessionManager()); + + + @Test + public void createBinder() throws Exception { + BindingContext context = createBindingContext("initBinder", WebDataBinder.class); + WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, null); + + assertNotNull(dataBinder.getDisallowedFields()); + assertEquals("id", dataBinder.getDisallowedFields()[0]); + } + + @Test + public void createBinderWithGlobalInitialization() throws Exception { + ConversionService conversionService = new DefaultFormattingConversionService(); + bindingInitializer.setConversionService(conversionService); + + BindingContext context = createBindingContext("initBinder", WebDataBinder.class); + WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, null); + + assertSame(conversionService, dataBinder.getConversionService()); + } + + @Test + public void createBinderWithAttrName() throws Exception { + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, "foo"); + + assertNotNull(dataBinder.getDisallowedFields()); + assertEquals("id", dataBinder.getDisallowedFields()[0]); + } + + @Test + public void createBinderWithAttrNameNoMatch() throws Exception { + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, "invalidName"); + + assertNull(dataBinder.getDisallowedFields()); + } + + @Test + public void createBinderNullAttrName() throws Exception { + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, null); + + assertNull(dataBinder.getDisallowedFields()); + } + + @Test(expected = IllegalStateException.class) + public void returnValueNotExpected() throws Exception { + BindingContext context = createBindingContext("initBinderReturnValue", WebDataBinder.class); + context.createDataBinder(this.exchange, null, "invalidName"); + } + + @Test + public void createBinderTypeConversion() throws Exception { + this.exchange.getRequest().getQueryParams().add("requestParam", "22"); + this.argumentResolvers.add(new RequestParamMethodArgumentResolver(null, false)); + + BindingContext context = createBindingContext("initBinderTypeConversion", WebDataBinder.class, int.class); + WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, "foo"); + + assertNotNull(dataBinder.getDisallowedFields()); + assertEquals("requestParam-22", dataBinder.getDisallowedFields()[0]); + } + + + private BindingContext createBindingContext(String methodName, Class... parameterTypes) + throws Exception { + + Object handler = new InitBinderHandler(); + Method method = handler.getClass().getMethod(methodName, parameterTypes); + + SyncInvocableHandlerMethod handlerMethod = new SyncInvocableHandlerMethod(handler, method); + handlerMethod.setArgumentResolvers(new ArrayList<>(this.argumentResolvers)); + handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + + return new InitBinderBindingContext( + this.bindingInitializer, Collections.singletonList(handlerMethod)); + } + + + private static class InitBinderHandler { + + @InitBinder + public void initBinder(WebDataBinder dataBinder) { + dataBinder.setDisallowedFields("id"); + } + + @InitBinder(value="foo") + public void initBinderWithAttributeName(WebDataBinder dataBinder) { + dataBinder.setDisallowedFields("id"); + } + + @InitBinder + public String initBinderReturnValue(WebDataBinder dataBinder) { + return "invalid"; + } + + @InitBinder + public void initBinderTypeConversion(WebDataBinder dataBinder, @RequestParam int requestParam) { + dataBinder.setDisallowedFields("requestParam-" + requestParam); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingDataBindingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingDataBindingIntegrationTests.java new file mode 100644 index 0000000000..a85acf2122 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingDataBindingIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * 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.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Test; + +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.reactive.config.EnableWebReactive; + +import static org.junit.Assert.assertEquals; + + +/** + * Data binding and type conversion related integration tests for + * {@code @Controller}-annotated classes. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingDataBindingIntegrationTests extends AbstractRequestMappingIntegrationTests { + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); + wac.refresh(); + return wac; + } + + + @Test + public void handleDateParam() throws Exception { + assertEquals("Processed date!", + performPost("/date-param?date=2016-10-31&date-pattern=YYYY-mm-dd", + new HttpHeaders(), null, String.class).getBody()); + } + + + @Configuration + @EnableWebReactive + @ComponentScan(resourcePattern = "**/RequestMappingDataBindingIntegrationTests*.class") + @SuppressWarnings({"unused", "WeakerAccess"}) + static class WebConfig { + } + + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @InitBinder + public void initBinder(WebDataBinder dataBinder, @RequestParam("date-pattern") String pattern) { + CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false); + dataBinder.registerCustomEditor(Date.class, dateEditor); + } + + @PostMapping("/date-param") + @ResponseBody + public String handleDateParam(@RequestParam Date date) { + return "Processed date!"; + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java b/spring-web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java index 0d58c140ad..6223005d1b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java @@ -39,12 +39,15 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory { private final List binderMethods; + /** * Create a new instance. * @param binderMethods {@code @InitBinder} methods, or {@code null} * @param initializer for global data binder intialization */ - public InitBinderDataBinderFactory(List binderMethods, WebBindingInitializer initializer) { + public InitBinderDataBinderFactory(List binderMethods, + WebBindingInitializer initializer) { + super(initializer); this.binderMethods = (binderMethods != null) ? binderMethods : new ArrayList<>(); } @@ -61,20 +64,20 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory { if (isBinderMethodApplicable(binderMethod, binder)) { Object returnValue = binderMethod.invokeForRequest(request, null, binder); if (returnValue != null) { - throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod); + throw new IllegalStateException( + "@InitBinder methods should return void: " + binderMethod); } } } } /** - * Return {@code true} if the given {@code @InitBinder} method should be - * invoked to initialize the given WebDataBinder. - *

The default implementation checks if target object name is included - * in the attribute names specified in the {@code @InitBinder} annotation. + * Whether the given {@code @InitBinder} method should be used to initialize + * the given WebDataBinder instance. By default we check the attributes + * names of the annotation, if present. */ - protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) { - InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class); + protected boolean isBinderMethodApplicable(HandlerMethod binderMethod, WebDataBinder binder) { + InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class); Collection names = Arrays.asList(annot.value()); return (names.size() == 0 || names.contains(binder.getObjectName())); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java index 6ee180cb8f..08bcdd4fc8 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java @@ -17,9 +17,8 @@ package org.springframework.web.method.annotation; import java.lang.reflect.Method; -import java.util.Arrays; +import java.util.Collections; -import org.junit.Before; import org.junit.Test; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; @@ -37,7 +36,10 @@ import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; import org.springframework.web.method.support.InvocableHandlerMethod; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; /** * Test fixture with {@link InitBinderDataBinderFactory}. @@ -46,23 +48,19 @@ import static org.junit.Assert.*; */ public class InitBinderDataBinderFactoryTests { - private ConfigurableWebBindingInitializer bindingInitializer; + private final ConfigurableWebBindingInitializer bindingInitializer = + new ConfigurableWebBindingInitializer(); - private HandlerMethodArgumentResolverComposite argumentResolvers; + private final HandlerMethodArgumentResolverComposite argumentResolvers = + new HandlerMethodArgumentResolverComposite(); - private NativeWebRequest webRequest; + private final NativeWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest()); - @Before - public void setUp() throws Exception { - bindingInitializer = new ConfigurableWebBindingInitializer(); - argumentResolvers = new HandlerMethodArgumentResolverComposite(); - webRequest = new ServletWebRequest(new MockHttpServletRequest()); - } @Test public void createBinder() throws Exception { - WebDataBinderFactory factory = createBinderFactory("initBinder", WebDataBinder.class); - WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); + WebDataBinderFactory factory = createFactory("initBinder", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, null); assertNotNull(dataBinder.getDisallowedFields()); assertEquals("id", dataBinder.getDisallowedFields()[0]); @@ -73,16 +71,16 @@ public class InitBinderDataBinderFactoryTests { ConversionService conversionService = new DefaultFormattingConversionService(); bindingInitializer.setConversionService(conversionService); - WebDataBinderFactory factory = createBinderFactory("initBinder", WebDataBinder.class); - WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); + WebDataBinderFactory factory = createFactory("initBinder", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, null); assertSame(conversionService, dataBinder.getConversionService()); } @Test public void createBinderWithAttrName() throws Exception { - WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); - WebDataBinder dataBinder = factory.createBinder(webRequest, null, "foo"); + WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "foo"); assertNotNull(dataBinder.getDisallowedFields()); assertEquals("id", dataBinder.getDisallowedFields()[0]); @@ -90,52 +88,54 @@ public class InitBinderDataBinderFactoryTests { @Test public void createBinderWithAttrNameNoMatch() throws Exception { - WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); - WebDataBinder dataBinder = factory.createBinder(webRequest, null, "invalidName"); + WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "invalidName"); assertNull(dataBinder.getDisallowedFields()); } @Test public void createBinderNullAttrName() throws Exception { - WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); - WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); + WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, null); assertNull(dataBinder.getDisallowedFields()); } @Test(expected = IllegalStateException.class) public void returnValueNotExpected() throws Exception { - WebDataBinderFactory factory = createBinderFactory("initBinderReturnValue", WebDataBinder.class); - factory.createBinder(webRequest, null, "invalidName"); + WebDataBinderFactory factory = createFactory("initBinderReturnValue", WebDataBinder.class); + factory.createBinder(this.webRequest, null, "invalidName"); } @Test public void createBinderTypeConversion() throws Exception { - webRequest.getNativeRequest(MockHttpServletRequest.class).setParameter("requestParam", "22"); - argumentResolvers.addResolver(new RequestParamMethodArgumentResolver(null, false)); + this.webRequest.getNativeRequest(MockHttpServletRequest.class).setParameter("requestParam", "22"); + this.argumentResolvers.addResolver(new RequestParamMethodArgumentResolver(null, false)); - WebDataBinderFactory factory = createBinderFactory("initBinderTypeConversion", WebDataBinder.class, int.class); - WebDataBinder dataBinder = factory.createBinder(webRequest, null, "foo"); + WebDataBinderFactory factory = createFactory("initBinderTypeConversion", WebDataBinder.class, int.class); + WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "foo"); assertNotNull(dataBinder.getDisallowedFields()); assertEquals("requestParam-22", dataBinder.getDisallowedFields()[0]); } - private WebDataBinderFactory createBinderFactory(String methodName, Class... parameterTypes) + private WebDataBinderFactory createFactory(String methodName, Class... parameterTypes) throws Exception { Object handler = new InitBinderHandler(); Method method = handler.getClass().getMethod(methodName, parameterTypes); InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); - handlerMethod.setHandlerMethodArgumentResolvers(argumentResolvers); + handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); handlerMethod.setDataBinderFactory(new DefaultDataBinderFactory(null)); handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); - return new InitBinderDataBinderFactory(Arrays.asList(handlerMethod), bindingInitializer); + return new InitBinderDataBinderFactory( + Collections.singletonList(handlerMethod), this.bindingInitializer); } + private static class InitBinderHandler { @InitBinder