Support for populating model attributes through data class constructors

Includes a new overloaded ModelAndView constructor with an HttpStatus argument, as well as a HandlerMethodArgumentResolverSupport refactoring (revised checkParameterType signature, actually implementing the HandlerMethodArgumentResolver interface).

Issue: SPR-15199
This commit is contained in:
Juergen Hoeller
2017-03-24 12:15:45 +01:00
parent b3154357f0
commit 65ba865d70
30 changed files with 411 additions and 274 deletions

View File

@@ -16,15 +16,20 @@
package org.springframework.web.method.annotation;
import java.beans.ConstructorProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
@@ -52,10 +57,13 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* attribute with or without the presence of an {@code @ModelAttribute}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 3.1
*/
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
protected final Log logger = LogFactory.getLog(getClass());
private final boolean annotationNotRequired;
@@ -65,7 +73,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
* Class constructor.
* @param annotationNotRequired if "true", non-simple method arguments and
* return values are considered model attributes with or without a
* {@code @ModelAttribute} annotation.
* {@code @ModelAttribute} annotation
*/
public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
this.annotationNotRequired = annotationNotRequired;
@@ -89,8 +97,8 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
* with request values via data binding and optionally validated
* if {@code @java.validation.Valid} is present on the argument.
* @throws BindException if data binding and validation result in an error
* and the next method parameter is not of type {@link Errors}.
* @throws Exception if WebDataBinder initialization fails.
* and the next method parameter is not of type {@link Errors}
* @throws Exception if WebDataBinder initialization fails
*/
@Override
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
@@ -123,22 +131,54 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
return (parameter.getParameterType().isInstance(attribute) ? attribute :
binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter));
}
/**
* Extension point to create the model attribute if not found in the model.
* The default implementation uses the default constructor.
* Extension point to create the model attribute if not found in the model,
* with subsequent parameter binding through bean properties (unless suppressed).
* <p>The default implementation uses the unique public no-arg constructor, if any,
* which may have arguments: It understands the JavaBeans {@link ConstructorProperties}
* annotation as well as runtime-retained parameter names in the bytecode,
* associating request parameters with constructor arguments by name. If no such
* constructor is found, the default constructor will be used (even if not public),
* assuming subsequent bean property bindings through setter methods.
* @param attributeName the name of the attribute (never {@code null})
* @param methodParam the method parameter
* @param parameter the method parameter declaration
* @param binderFactory for creating WebDataBinder instance
* @param request the current request
* @param webRequest the current request
* @return the created model attribute (never {@code null})
*/
protected Object createAttribute(String attributeName, MethodParameter methodParam,
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
protected Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
return BeanUtils.instantiateClass(methodParam.getParameterType());
Constructor<?>[] ctors = parameter.getParameterType().getConstructors();
if (ctors.length != 1) {
// No standard data class or standard JavaBeans arrangement ->
// defensively go with default constructor, expecting regular bean property bindings.
return BeanUtils.instantiateClass(parameter.getParameterType());
}
Constructor<?> ctor = ctors[0];
if (ctor.getParameterCount() == 0) {
// A single default constructor -> clearly a standard JavaBeans arrangement.
return BeanUtils.instantiateClass(ctor);
}
// A single data class constructor -> resolve constructor arguments from request parameters.
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
Class<?>[] paramTypes = ctor.getParameterTypes();
Assert.state(paramNames.length == paramTypes.length,
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
Object[] args = new Object[paramTypes.length];
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
for (int i = 0; i < paramNames.length; i++) {
args[i] = binder.convertIfNecessary(
webRequest.getParameterValues(paramNames[i]), paramTypes[i], new MethodParameter(ctor, i));
}
return BeanUtils.instantiateClass(ctor, args);
}
/**
@@ -156,10 +196,10 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
* Spring's {@link org.springframework.validation.annotation.Validated},
* and custom annotations whose name starts with "Valid".
* @param binder the DataBinder to be used
* @param methodParam the method parameter
* @param parameter the method parameter declaration
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
Annotation[] annotations = methodParam.getParameterAnnotations();
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
@@ -174,12 +214,12 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
/**
* Whether to raise a fatal bind exception on validation errors.
* @param binder the data binder used to perform data binding
* @param methodParam the method argument
* @param parameter the method parameter declaration
* @return {@code true} if the next method argument is not of type {@link Errors}
*/
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
int i = methodParam.getParameterIndex();
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
int i = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
return !hasBindingResult;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -70,7 +70,7 @@ public class ModelAttributeMethodProcessorTests {
@Before
public void setUp() throws Exception {
public void setup() throws Exception {
this.request = new ServletWebRequest(new MockHttpServletRequest());
this.container = new ModelAndViewContainer();
this.processor = new ModelAttributeMethodProcessor(false);
@@ -145,7 +145,7 @@ public class ModelAttributeMethodProcessorTests {
}
@Test
public void resovleArgumentViaDefaultConstructor() throws Exception {
public void resolveArgumentViaDefaultConstructor() throws Exception {
WebDataBinder dataBinder = new WebRequestDataBinder(null);
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
given(factory.createBinder(any(), notNull(), eq("attrName"))).willReturn(dataBinder);

View File

@@ -44,7 +44,7 @@ public interface HandlerMethodArgumentResolver {
* @param exchange the current exchange
* @return {@code Mono} for the argument value, possibly empty
*/
Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
ServerWebExchange exchange);
Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange);
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.method;
import java.lang.annotation.Annotation;
@@ -25,14 +26,14 @@ import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.util.Assert;
/**
* Base class for {@link HandlerMethodArgumentResolver} implementations with
* access to a {@code ReactiveAdapterRegistry} and methods to check for
* method parameter support.
* Base class for {@link HandlerMethodArgumentResolver} implementations with access to a
* {@code ReactiveAdapterRegistry} and methods to check for method parameter support.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0
*/
public abstract class HandlerMethodArgumentResolverSupport {
public abstract class HandlerMethodArgumentResolverSupport implements HandlerMethodArgumentResolver {
private final ReactiveAdapterRegistry adapterRegistry;
@@ -55,12 +56,12 @@ public abstract class HandlerMethodArgumentResolverSupport {
* Evaluate the {@code Predicate} on the the method parameter type or on
* the generic type within a reactive type wrapper.
*/
protected boolean checkParamType(MethodParameter param, Predicate<Class<?>> predicate) {
Class<?> type = param.getParameterType();
protected boolean checkParameterType(MethodParameter parameter, Predicate<Class<?>> predicate) {
Class<?> type = parameter.getParameterType();
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type);
if (adapter != null) {
assertHasValues(adapter, param);
type = param.nested().getNestedParameterType();
assertHasValues(adapter, parameter);
type = parameter.nested().getNestedParameterType();
}
return predicate.test(type);
}
@@ -77,25 +78,25 @@ public abstract class HandlerMethodArgumentResolverSupport {
* {@code IllegalStateException} if the same matches the generic type
* within a reactive type wrapper.
*/
protected boolean checkParamTypeNoReactiveWrapper(MethodParameter param, Predicate<Class<?>> predicate) {
Class<?> type = param.getParameterType();
protected boolean checkParameterTypeNoReactiveWrapper(MethodParameter parameter, Predicate<Class<?>> predicate) {
Class<?> type = parameter.getParameterType();
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type);
if (adapter != null) {
assertHasValues(adapter, param);
type = param.nested().getNestedParameterType();
assertHasValues(adapter, parameter);
type = parameter.nested().getNestedParameterType();
}
if (predicate.test(type)) {
if (adapter == null) {
return true;
}
throw getReactiveWrapperError(param);
throw buildReactiveWrapperException(parameter);
}
return false;
}
private IllegalStateException getReactiveWrapperError(MethodParameter param) {
private IllegalStateException buildReactiveWrapperException(MethodParameter parameter) {
return new IllegalStateException(getClass().getSimpleName() +
" doesn't support reactive type wrapper: " + param.getGenericParameterType());
" doesn't support reactive type wrapper: " + parameter.getGenericParameterType());
}
/**
@@ -105,29 +106,28 @@ public abstract class HandlerMethodArgumentResolverSupport {
* type within a reactive type wrapper.
*/
protected <A extends Annotation> boolean checkAnnotatedParamNoReactiveWrapper(
MethodParameter param, Class<A> annotationType,
BiPredicate<A, Class<?>> typePredicate) {
MethodParameter parameter, Class<A> annotationType, BiPredicate<A, Class<?>> typePredicate) {
A annotation = param.getParameterAnnotation(annotationType);
A annotation = parameter.getParameterAnnotation(annotationType);
if (annotation == null) {
return false;
}
param = param.nestedIfOptional();
Class<?> type = param.getNestedParameterType();
parameter = parameter.nestedIfOptional();
Class<?> type = parameter.getNestedParameterType();
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type);
if (adapter != null) {
assertHasValues(adapter, param);
param = param.nested();
type = param.getNestedParameterType();
assertHasValues(adapter, parameter);
parameter = parameter.nested();
type = parameter.getNestedParameterType();
}
if (typePredicate.test(annotation, type)) {
if (adapter == null) {
return true;
}
throw getReactiveWrapperError(param);
throw buildReactiveWrapperException(parameter);
}
return false;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -33,15 +33,14 @@ import org.springframework.web.server.ServerWebExchange;
*/
public interface SyncHandlerMethodArgumentResolver extends HandlerMethodArgumentResolver {
/**
* {@inheritDoc}
* <p>By default this simply delegates to {@link #resolveArgumentValue} for
* synchronous resolution.
*/
@Override
default Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
ServerWebExchange exchange) {
default Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
return Mono.justOrEmpty(resolveArgumentValue(parameter, bindingContext, exchange));
}
@@ -53,7 +52,7 @@ public interface SyncHandlerMethodArgumentResolver extends HandlerMethodArgument
* @param exchange the current exchange
* @return an {@code Optional} with the resolved value, possibly empty
*/
Optional<Object> resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext,
ServerWebExchange exchange);
Optional<Object> resolveArgumentValue(
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -114,7 +114,6 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
}
for (ServerHttpMessageReader<?> reader : getMessageReaders()) {
if (reader.canRead(elementType, mediaType)) {
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
@@ -171,9 +170,9 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
private Object[] extractValidationHints(MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann));
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
}
}

View File

@@ -32,7 +32,6 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebExchange;
@@ -58,8 +57,7 @@ import org.springframework.web.server.ServerWebInputException;
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodArgumentResolverSupport
implements HandlerMethodArgumentResolver {
public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodArgumentResolverSupport {
private final ConfigurableBeanFactory configurableBeanFactory;
@@ -69,23 +67,21 @@ public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodAr
/**
* @param beanFactory a bean factory to use for resolving ${...} placeholder
* @param factory a bean factory to use for resolving ${...} placeholder
* and #{...} SpEL expressions in default values, or {@code null} if default
* values are not expected to contain expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
public AbstractNamedValueArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
this.configurableBeanFactory = beanFactory;
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
public AbstractNamedValueArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(registry);
this.configurableBeanFactory = factory;
this.expressionContext = (factory != null ? new BeanExpressionContext(factory, null) : null);
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
ServerWebExchange exchange) {
public Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
@@ -175,8 +171,7 @@ public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodAr
* @param exchange the current exchange
* @return the resolved argument (may be {@code null})
*/
protected abstract Mono<Object> resolveName(String name, MethodParameter parameter,
ServerWebExchange exchange);
protected abstract Mono<Object> resolveName(String name, MethodParameter parameter, ServerWebExchange exchange);
/**
* Apply type conversion if necessary.
@@ -277,8 +272,8 @@ public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodAr
* @param exchange the current exchange
*/
@SuppressWarnings("UnusedParameters")
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
Model model, ServerWebExchange exchange) {
protected void handleResolvedValue(
Object arg, String name, MethodParameter parameter, Model model, ServerWebExchange exchange) {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -39,23 +39,20 @@ import org.springframework.web.server.ServerWebExchange;
public abstract class AbstractNamedValueSyncArgumentResolver extends AbstractNamedValueArgumentResolver
implements SyncHandlerMethodArgumentResolver {
/**
* @param beanFactory a bean factory to use for resolving ${...}
* @param factory a bean factory to use for resolving ${...}
* placeholder and #{...} SpEL expressions in default values;
* or {@code null} if default values are not expected to have expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
protected AbstractNamedValueSyncArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
protected AbstractNamedValueSyncArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
ServerWebExchange exchange) {
public Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
// Flip the default implementation from SyncHandlerMethodArgumentResolver:
// instead of delegating to (sync) resolveArgumentValue,
@@ -66,8 +63,8 @@ public abstract class AbstractNamedValueSyncArgumentResolver extends AbstractNam
}
@Override
public Optional<Object> resolveArgumentValue(MethodParameter parameter,
BindingContext context, ServerWebExchange exchange) {
public Optional<Object> resolveArgumentValue(
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
// This won't block since resolveName below doesn't
Object value = resolveArgument(parameter, context, exchange).block();
@@ -76,16 +73,13 @@ public abstract class AbstractNamedValueSyncArgumentResolver extends AbstractNam
}
@Override
protected final Mono<Object> resolveName(String name, MethodParameter param,
ServerWebExchange exchange) {
protected final Mono<Object> resolveName(String name, MethodParameter param, ServerWebExchange exchange) {
return Mono.justOrEmpty(resolveNamedValue(name, param, exchange));
}
/**
* Actually resolve the value synchronously.
*/
protected abstract Optional<Object> resolveNamedValue(String name,
MethodParameter param, ServerWebExchange exchange);
protected abstract Optional<Object> resolveNamedValue(String name, MethodParameter param, ServerWebExchange exchange);
}

View File

@@ -38,17 +38,14 @@ import org.springframework.web.server.ServerWebInputException;
*/
public class CookieValueMethodArgumentResolver extends AbstractNamedValueSyncArgumentResolver {
/**
* @param beanFactory a bean factory to use for resolving ${...}
* @param factory a bean factory to use for resolving ${...}
* placeholder and #{...} SpEL expressions in default values;
* or {@code null} if default values are not expected to contain expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
public CookieValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
public CookieValueMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}
@@ -64,9 +61,7 @@ public class CookieValueMethodArgumentResolver extends AbstractNamedValueSyncArg
}
@Override
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter,
ServerWebExchange exchange) {
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
HttpCookie cookie = exchange.getRequest().getCookies().getFirst(name);
Class<?> paramType = parameter.getNestedParameterType();
if (HttpCookie.class.isAssignableFrom(paramType)) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -41,9 +41,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverSupport
implements HandlerMethodArgumentResolver {
public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverSupport {
public ErrorsMethodArgumentResolver(ReactiveAdapterRegistry registry) {
super(registry);
@@ -52,13 +50,13 @@ public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverS
@Override
public boolean supportsParameter(MethodParameter parameter) {
return checkParamTypeNoReactiveWrapper(parameter, Errors.class::isAssignableFrom);
return checkParameterTypeNoReactiveWrapper(parameter, Errors.class::isAssignableFrom);
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext context,
ServerWebExchange exchange) {
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);
@@ -79,9 +77,8 @@ public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverS
}
private String getModelAttributeName(MethodParameter parameter) {
Assert.isTrue(parameter.getParameterIndex() > 0,
"Errors argument must be immediately after a model attribute argument.");
"Errors argument must be immediately after a model attribute argument");
int index = parameter.getParameterIndex() - 1;
MethodParameter attributeParam = new MethodParameter(parameter.getMethod(), index);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -36,17 +36,14 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueSyncArgumentResolver {
/**
* @param beanFactory a bean factory to use for resolving ${...}
* @param factory a bean factory to use for resolving ${...}
* placeholder and #{...} SpEL expressions in default values;
* or {@code null} if default values are not expected to contain expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
public ExpressionValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
public ExpressionValueMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}
@@ -62,9 +59,7 @@ public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueSyn
}
@Override
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter,
ServerWebExchange exchange) {
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
// No name to resolve
return Optional.empty();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -38,9 +38,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentResolver
implements HandlerMethodArgumentResolver {
public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentResolver {
public HttpEntityArgumentResolver(List<ServerHttpMessageReader<?>> readers,
ReactiveAdapterRegistry registry) {
@@ -51,16 +49,15 @@ public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentRes
@Override
public boolean supportsParameter(MethodParameter parameter) {
return checkParamTypeNoReactiveWrapper(parameter,
return checkParameterTypeNoReactiveWrapper(parameter,
type -> HttpEntity.class.equals(type) || RequestEntity.class.equals(type));
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
ServerWebExchange exchange) {
public Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
Class<?> entityType = parameter.getParameterType();
return readBody(parameter.nested(), false, bindingContext, exchange)
.map(body -> createEntity(body, entityType, exchange.getRequest()))
.defaultIfEmpty(createEntity(null, entityType, exchange.getRequest()));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -36,7 +36,6 @@ import org.springframework.web.server.ServerWebExchange;
public class ModelArgumentResolver extends HandlerMethodArgumentResolverSupport
implements SyncHandlerMethodArgumentResolver {
public ModelArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
}
@@ -44,7 +43,7 @@ public class ModelArgumentResolver extends HandlerMethodArgumentResolverSupport
@Override
public boolean supportsParameter(MethodParameter parameter) {
return checkParamTypeNoReactiveWrapper(parameter, Model.class::isAssignableFrom);
return checkParameterTypeNoReactiveWrapper(parameter, Model.class::isAssignableFrom);
}
@Override

View File

@@ -16,30 +16,34 @@
package org.springframework.web.reactive.result.method.annotation;
import java.beans.ConstructorProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import org.springframework.beans.BeanUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.ui.Model;
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.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport;
import org.springframework.web.server.ServerWebExchange;
@@ -58,10 +62,12 @@ import org.springframework.web.server.ServerWebExchange;
* attribute with or without the presence of an {@code @ModelAttribute}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0
*/
public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport
implements HandlerMethodArgumentResolver {
public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final boolean useDefaultResolution;
@@ -87,25 +93,25 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR
return true;
}
else if (this.useDefaultResolution) {
return checkParamType(parameter, type -> !BeanUtils.isSimpleProperty(type));
return checkParameterType(parameter, type -> !BeanUtils.isSimpleProperty(type));
}
return false;
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext context,
ServerWebExchange exchange) {
public Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
ResolvableType type = ResolvableType.forMethodParameter(parameter);
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type.resolve());
ResolvableType valueType = (adapter != null ? type.getGeneric(0) : type);
Assert.state(adapter == null || !adapter.isMultiValue(),
getClass().getSimpleName() + " doesn't support multi-value reactive type wrapper: " +
() -> getClass().getSimpleName() + " doesn't support multi-value reactive type wrapper: " +
parameter.getGenericParameterType());
String name = getAttributeName(valueType, parameter);
Mono<?> valueMono = getAttributeMono(name, valueType, context.getModel());
Mono<?> valueMono = getAttributeMono(name, valueType, context, exchange);
Map<String, Object> model = context.getModel().asMap();
MonoProcessor<BindingResult> bindingResultMono = MonoProcessor.create();
@@ -140,22 +146,25 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR
}
private String getAttributeName(ResolvableType valueType, MethodParameter parameter) {
ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
if (annot != null && StringUtils.hasText(annot.value())) {
return annot.value();
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null && StringUtils.hasText(ann.value())) {
return ann.value();
}
// TODO: Conventions does not deal with async wrappers
return ClassUtils.getShortNameAsProperty(valueType.getRawClass());
}
private Mono<?> getAttributeMono(String attributeName, ResolvableType attributeType, Model model) {
Object attribute = model.asMap().get(attributeName);
private Mono<?> getAttributeMono(
String attributeName, ResolvableType attributeType, BindingContext context, ServerWebExchange exchange) {
Object attribute = context.getModel().asMap().get(attributeName);
if (attribute == null) {
attribute = BeanUtils.instantiateClass(attributeType.getRawClass());
return createAttribute(attributeName, attributeType.getRawClass(), context, exchange);
}
ReactiveAdapter adapterFrom = getAdapterRegistry().getAdapter(null, attribute);
if (adapterFrom != null) {
Assert.isTrue(!adapterFrom.isMultiValue(), "Data binding supports single-value async types.");
Assert.isTrue(!adapterFrom.isMultiValue(), "Data binding only supports single-value async types");
return Mono.from(adapterFrom.toPublisher(attribute));
}
else {
@@ -163,10 +172,48 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR
}
}
private boolean hasErrorsArgument(MethodParameter methodParam) {
int i = methodParam.getParameterIndex();
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
return paramTypes.length > i && Errors.class.isAssignableFrom(paramTypes[i + 1]);
private Mono<?> createAttribute(
String attributeName, Class<?> attributeType, BindingContext context, ServerWebExchange exchange) {
Constructor<?>[] ctors = attributeType.getConstructors();
if (ctors.length != 1) {
// No standard data class or standard JavaBeans arrangement ->
// defensively go with default constructor, expecting regular bean property bindings.
return Mono.just(BeanUtils.instantiateClass(attributeType));
}
Constructor<?> ctor = ctors[0];
if (ctor.getParameterCount() == 0) {
// A single default constructor -> clearly a standard JavaBeans arrangement.
return Mono.just(BeanUtils.instantiateClass(ctor));
}
// A single data class constructor -> resolve constructor arguments from request parameters.
return exchange.getRequestParams().then(requestParams -> {
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
Class<?>[] paramTypes = ctor.getParameterTypes();
Assert.state(paramNames.length == paramTypes.length,
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
Object[] args = new Object[paramTypes.length];
WebDataBinder binder = context.createDataBinder(exchange, null, attributeName);
for (int i = 0; i < paramNames.length; i++) {
List<String> paramValues = requestParams.get(paramNames[i]);
Object paramValue = null;
if (paramValues != null) {
paramValue = (paramValues.size() == 1 ? paramValues.get(0) :
paramValues.toArray(new String[paramValues.size()]));
}
args[i] = binder.convertIfNecessary(paramValue, paramTypes[i], new MethodParameter(ctor, i));
}
return Mono.fromSupplier(() -> BeanUtils.instantiateClass(ctor, args));
});
}
private boolean hasErrorsArgument(MethodParameter parameter) {
int i = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
return (paramTypes.length > i && Errors.class.isAssignableFrom(paramTypes[i + 1]));
}
private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -43,7 +43,6 @@ import org.springframework.web.server.ServerWebExchange;
public class PathVariableMapMethodArgumentResolver extends HandlerMethodArgumentResolverSupport
implements SyncHandlerMethodArgumentResolver {
public PathVariableMapMethodArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -52,17 +52,14 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class PathVariableMethodArgumentResolver extends AbstractNamedValueSyncArgumentResolver {
/**
* @param beanFactory a bean factory to use for resolving ${...}
* @param factory a bean factory to use for resolving ${...}
* placeholder and #{...} SpEL expressions in default values;
* or {@code null} if default values are not expected to contain expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
public PathVariableMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
public PathVariableMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}
@@ -83,9 +80,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueSyncAr
@Override
@SuppressWarnings("unchecked")
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter,
ServerWebExchange exchange) {
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
String attributeName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
return exchange.getAttribute(attributeName)
.map(value -> ((Map<String, String>) value).get(name));
@@ -98,8 +93,8 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueSyncAr
@Override
@SuppressWarnings("unchecked")
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
Model model, ServerWebExchange exchange) {
protected void handleResolvedValue(
Object arg, String name, MethodParameter parameter, Model model, ServerWebExchange exchange) {
// TODO: View.PATH_VARIABLES ?
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -24,7 +24,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.util.Assert;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport;
import org.springframework.web.server.ServerWebExchange;
@@ -35,9 +34,7 @@ import org.springframework.web.server.ServerWebExchange;
* @since 5.0
* @see ServerWebExchangeArgumentResolver
*/
public class PrincipalArgumentResolver extends HandlerMethodArgumentResolverSupport
implements HandlerMethodArgumentResolver {
public class PrincipalArgumentResolver extends HandlerMethodArgumentResolverSupport {
public PrincipalArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
@@ -46,7 +43,7 @@ public class PrincipalArgumentResolver extends HandlerMethodArgumentResolverSupp
@Override
public boolean supportsParameter(MethodParameter parameter) {
return checkParamTypeNoReactiveWrapper(parameter, Principal.class::isAssignableFrom);
return checkParameterTypeNoReactiveWrapper(parameter, Principal.class::isAssignableFrom);
}
@Override

View File

@@ -36,15 +36,13 @@ public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueSy
/**
* @param beanFactory a bean factory to use for resolving ${...}
* @param factory a bean factory to use for resolving ${...}
* placeholder and #{...} SpEL expressions in default values;
* or {@code null} if default values are not expected to have expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
public RequestAttributeMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
public RequestAttributeMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}
@@ -61,9 +59,7 @@ public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueSy
}
@Override
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter,
ServerWebExchange exchange) {
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
return exchange.getAttribute(name);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -43,9 +43,7 @@ import org.springframework.web.server.ServerWebInputException;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class RequestBodyArgumentResolver extends AbstractMessageReaderArgumentResolver
implements HandlerMethodArgumentResolver {
public class RequestBodyArgumentResolver extends AbstractMessageReaderArgumentResolver {
public RequestBodyArgumentResolver(List<ServerHttpMessageReader<?>> readers,
ReactiveAdapterRegistry registry) {
@@ -60,8 +58,8 @@ public class RequestBodyArgumentResolver extends AbstractMessageReaderArgumentRe
}
@Override
public Mono<Object> resolveArgument(MethodParameter param, BindingContext bindingContext,
ServerWebExchange exchange) {
public Mono<Object> resolveArgument(
MethodParameter param, BindingContext bindingContext, ServerWebExchange exchange) {
RequestBody annotation = param.getParameterAnnotation(RequestBody.class);
return readBody(param, annotation.required(), bindingContext, exchange);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -45,7 +45,6 @@ import org.springframework.web.server.ServerWebExchange;
public class RequestHeaderMapMethodArgumentResolver extends HandlerMethodArgumentResolverSupport
implements SyncHandlerMethodArgumentResolver {
public RequestHeaderMapMethodArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
}

View File

@@ -47,15 +47,13 @@ import org.springframework.web.server.ServerWebInputException;
public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueSyncArgumentResolver {
/**
* @param beanFactory a bean factory to use for resolving ${...}
* @param factory a bean factory to use for resolving ${...}
* placeholder and #{...} SpEL expressions in default values;
* or {@code null} if default values are not expected to have expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
*/
public RequestHeaderMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
public RequestHeaderMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}

View File

@@ -48,7 +48,6 @@ import org.springframework.web.server.ServerWebExchange;
public class RequestParamMapMethodArgumentResolver extends HandlerMethodArgumentResolverSupport
implements SyncHandlerMethodArgumentResolver {
public RequestParamMapMethodArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -59,19 +59,19 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueSyncAr
/**
* Class constructor with a default resolution mode flag.
* @param beanFactory a bean factory used for resolving ${...} placeholder
* @param factory a bean factory used for resolving ${...} placeholder
* and #{...} SpEL expressions in default values, or {@code null} if default
* values are not expected to contain expressions
* @param adapterRegistry for checking reactive type wrappers
* @param registry for checking reactive type wrappers
* @param useDefaultResolution in default resolution mode a method argument
* that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
* is treated as a request parameter even if it isn't annotated, the
* request parameter name is derived from the method parameter name.
*/
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry, boolean useDefaultResolution) {
public RequestParamMethodArgumentResolver(
ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry, boolean useDefaultResolution) {
super(beanFactory, adapterRegistry);
super(factory, registry);
this.useDefaultResolution = useDefaultResolution;
}
@@ -82,7 +82,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueSyncAr
return true;
}
else if (this.useDefaultResolution) {
return checkParamTypeNoReactiveWrapper(param, BeanUtils::isSimpleProperty);
return checkParameterTypeNoReactiveWrapper(param, BeanUtils::isSimpleProperty);
}
return false;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -48,7 +48,6 @@ import org.springframework.web.server.ServerWebExchange;
public class ServerWebExchangeArgumentResolver extends HandlerMethodArgumentResolverSupport
implements SyncHandlerMethodArgumentResolver {
public ServerWebExchangeArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
}
@@ -56,7 +55,7 @@ public class ServerWebExchangeArgumentResolver extends HandlerMethodArgumentReso
@Override
public boolean supportsParameter(MethodParameter parameter) {
return checkParamTypeNoReactiveWrapper(parameter,
return checkParameterTypeNoReactiveWrapper(parameter,
type -> ServerWebExchange.class.isAssignableFrom(type) ||
ServerHttpRequest.class.isAssignableFrom(type) ||
ServerHttpResponse.class.isAssignableFrom(type) ||

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -13,6 +13,7 @@
* 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.Optional;
@@ -36,11 +37,8 @@ import org.springframework.web.server.ServerWebInputException;
*/
public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueArgumentResolver {
public SessionAttributeMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
ReactiveAdapterRegistry adapterRegistry) {
super(beanFactory, adapterRegistry);
public SessionAttributeMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry) {
super(factory, registry);
}
@@ -56,9 +54,7 @@ public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueAr
}
@Override
protected Mono<Object> resolveName(String name, MethodParameter parameter,
ServerWebExchange exchange) {
protected Mono<Object> resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) {
return exchange.getSession()
.map(session -> session.getAttribute(name))
.filter(Optional::isPresent)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -34,9 +34,7 @@ import org.springframework.web.server.WebSession;
* @since 5.0
* @see ServerWebExchangeArgumentResolver
*/
public class WebSessionArgumentResolver extends HandlerMethodArgumentResolverSupport
implements HandlerMethodArgumentResolver {
public class WebSessionArgumentResolver extends HandlerMethodArgumentResolverSupport {
public WebSessionArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
@@ -45,12 +43,12 @@ public class WebSessionArgumentResolver extends HandlerMethodArgumentResolverSup
@Override
public boolean supportsParameter(MethodParameter parameter) {
return checkParamTypeNoReactiveWrapper(parameter, WebSession.class::isAssignableFrom);
return checkParameterTypeNoReactiveWrapper(parameter, WebSession.class::isAssignableFrom);
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext context,
ServerWebExchange exchange) {
public Mono<Object> resolveArgument(
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
Assert.isAssignable(WebSession.class, parameter.getParameterType());
return exchange.getSession().cast(Object.class);

View File

@@ -56,12 +56,9 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.post;
import static org.junit.Assert.*;
import static org.springframework.core.ResolvableType.*;
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.*;
/**
* Unit tests for {@link AbstractMessageReaderArgumentResolver}.
@@ -307,7 +304,16 @@ public class MessageReaderArgumentResolverTests {
private AbstractMessageReaderArgumentResolver resolver(Decoder<?>... decoders) {
List<ServerHttpMessageReader<?>> readers = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> readers.add(new DecoderHttpMessageReader<>(decoder)));
return new AbstractMessageReaderArgumentResolver(readers) {};
return new AbstractMessageReaderArgumentResolver(readers) {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return false;
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
return null;
}
};
}

View File

@@ -42,16 +42,13 @@ 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;
import static org.junit.Assert.*;
/**
* Unit tests for {@link ModelAttributeMethodArgumentResolver}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
*/
public class ModelAttributeMethodArgumentResolverTests {
@@ -116,7 +113,6 @@ public class ModelAttributeMethodArgumentResolverTests {
@Test
public void createAndBindToMono() throws Exception {
MethodParameter parameter = this.testMethod
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
@@ -130,7 +126,6 @@ public class ModelAttributeMethodArgumentResolverTests {
@Test
public void createAndBindToSingle() throws Exception {
MethodParameter parameter = this.testMethod
.annotPresent(ModelAttribute.class).arg(Single.class, Foo.class);
@@ -211,6 +206,7 @@ public class ModelAttributeMethodArgumentResolverTests {
Foo foo = valueExtractor.apply(value);
assertEquals("Robert", foo.getName());
assertEquals(25, foo.getAge());
String key = "foo";
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
@@ -231,7 +227,6 @@ public class ModelAttributeMethodArgumentResolverTests {
@Test
@SuppressWarnings("unchecked")
public void validationErrorToMono() throws Exception {
MethodParameter parameter = this.testMethod
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class);
@@ -246,7 +241,6 @@ public class ModelAttributeMethodArgumentResolverTests {
@Test
public void validationErrorToSingle() throws Exception {
MethodParameter parameter = this.testMethod
.annotPresent(ModelAttribute.class).arg(Single.class, Foo.class);
@@ -264,7 +258,6 @@ public class ModelAttributeMethodArgumentResolverTests {
ServerWebExchange exchange = postForm("age=invalid");
Mono<?> mono = createResolver().resolveArgument(param, this.bindContext, exchange);
mono = valueMonoExtractor.apply(mono);
StepVerifier.create(mono)
@@ -277,6 +270,31 @@ public class ModelAttributeMethodArgumentResolverTests {
.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);
@@ -298,7 +316,8 @@ public class ModelAttributeMethodArgumentResolverTests {
Foo fooNotAnnotated,
String stringNotAnnotated,
Mono<Foo> monoNotAnnotated,
Mono<String> monoStringNotAnnotated) {
Mono<String> monoStringNotAnnotated,
Bar barNotAnnotated) {
}
@@ -333,4 +352,35 @@ public class ModelAttributeMethodArgumentResolverTests {
}
}
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;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -89,7 +89,7 @@ public class ModelAndView {
}
/**
* Creates new ModelAndView given a view name and a model.
* Create a new ModelAndView given a view name and a model.
* @param viewName name of the View to render, to be resolved
* by the DispatcherServlet's ViewResolver
* @param model Map of model names (Strings) to model objects
@@ -104,7 +104,7 @@ public class ModelAndView {
}
/**
* Creates new ModelAndView given a View object and a model.
* Create a new ModelAndView given a View object and a model.
* <emphasis>Note: the supplied model data is copied into the internal
* storage of this class. You should not consider to modify the supplied
* Map after supplying it to this class</emphasis>
@@ -121,14 +121,27 @@ public class ModelAndView {
}
/**
* Creates new ModelAndView given a view name, model, and status.
* Create a new ModelAndView given a view name and HTTP status.
* @param viewName name of the View to render, to be resolved
* by the DispatcherServlet's ViewResolver
* @param status an HTTP status code to use for the response
* (to be set just prior to View rendering)
* @since 4.3.8
*/
public ModelAndView(String viewName, HttpStatus status) {
this.view = viewName;
this.status = status;
}
/**
* Create a new ModelAndView given a view name, model, and HTTP status.
* @param viewName name of the View to render, to be resolved
* by the DispatcherServlet's ViewResolver
* @param model Map of model names (Strings) to model objects
* (Objects). Model entries may not be {@code null}, but the
* model Map may be {@code null} if there is no model data.
* @param status an alternative status code to use for the response;
* The response status is set just prior to View rendering.
* @param status an HTTP status code to use for the response
* (to be set just prior to View rendering)
* @since 4.3
*/
public ModelAndView(String viewName, Map<String, ?> model, HttpStatus status) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -16,6 +16,7 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.beans.ConstructorProperties;
import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.io.Serializable;
@@ -140,14 +141,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
/**
* @author Rossen Stoyanchev
@@ -283,15 +277,13 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}, NestedSetController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do");
request.addParameter("testBeanSet", new String[] {"1", "2"});
request.addParameter("testBeanSet", "1", "2");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("[1, 2]-org.springframework.tests.sample.beans.TestBean", response.getContentAsString());
}
// SPR-12903
@Test
@Test // SPR-12903
public void pathVariableWithCustomConverter() throws Exception {
initServlet(new ApplicationContextInitializer<GenericWebApplicationContext>() {
@Override
@@ -1244,9 +1236,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertEquals("myParam-42", response.getContentAsString());
}
// SPR-9062
@Test
@Test // SPR-9062
public void ambiguousPathAndRequestMethod() throws Exception {
initServletWithControllers(AmbiguousPathAndRequestMethodController.class);
@@ -1498,9 +1488,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertEquals(405, response.getStatus());
}
// SPR-8536
@Test
@Test // SPR-8536
public void testHeadersCondition() throws Exception {
initServletWithControllers(HeadersConditionController.class);
@@ -1571,7 +1559,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertTrue(RequestContextUtils.getOutputFlashMap(request).isEmpty());
}
@Test // SPR-15176
@Test // SPR-15176
public void flashAttributesWithResponseEntity() throws Exception {
initServletWithControllers(RedirectAttributesController.class);
@@ -1831,6 +1819,31 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertTrue(response.getContentAsByteArray().length == 0);
}
@Test
public void dataClassBinding() throws ServletException, IOException {
initServletWithControllers(DataClassController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
request.addParameter("param1", "value1");
request.addParameter("param2", "2");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("value1-2-0", response.getContentAsString());
}
@Test
public void dataClassBindingWithAdditionalSetter() throws ServletException, IOException {
initServletWithControllers(DataClassController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
request.addParameter("param1", "value1");
request.addParameter("param2", "2");
request.addParameter("param3", "3");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals("value1-2-3", response.getContentAsString());
}
@Controller
static class ControllerWithEmptyValueMapping {
@@ -2727,15 +2740,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@XmlRootElement
public static class A {
}
@XmlRootElement
public static class B {
}
public static class NotReadableMessageConverter implements HttpMessageConverter<Object> {
@Override
@@ -3313,7 +3323,6 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
public HttpHeaders createNoHeader() throws URISyntaxException {
return new HttpHeaders();
}
}
@RestController
@@ -3345,7 +3354,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@RequestMapping("/path")
public ModelAndView methodWithHttpStatus(MyEntity object) {
return new ModelAndView("view", new ModelMap(), HttpStatus.UNPROCESSABLE_ENTITY);
return new ModelAndView("view", HttpStatus.UNPROCESSABLE_ENTITY);
}
@RequestMapping("/exception")
@@ -3355,7 +3364,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@ExceptionHandler(TestException.class)
public ModelAndView handleException() {
return new ModelAndView("view", new ModelMap(), HttpStatus.UNPROCESSABLE_ENTITY);
return new ModelAndView("view", HttpStatus.UNPROCESSABLE_ENTITY);
}
@SuppressWarnings("serial")
@@ -3363,4 +3372,32 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
}
public static class DataClass {
public final String param1;
public final int param2;
public int param3;
@ConstructorProperties({"param1", "param2"})
public DataClass(String param1, int p2) {
this.param1 = param1;
this.param2 = p2;
}
public void setParam3(int param3) {
this.param3 = param3;
}
}
@RestController
public static class DataClassController {
@RequestMapping("/bind")
public String handle(DataClass data) {
return data.param1 + "-" + data.param2 + "-" + data.param3;
}
}
}