SPR-8215 Move HandlerMethod code into trunk

This commit is contained in:
Rossen Stoyanchev
2011-04-06 11:30:59 +00:00
parent 0f7d43ba90
commit acc75aa4b8
86 changed files with 11635 additions and 13 deletions

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2002-2011 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.bind.support;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
/**
* A {@link WebDataBinderFactory} that creates {@link WebDataBinder} and initializes them
* with a {@link WebBindingInitializer}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class DefaultDataBinderFactory implements WebDataBinderFactory {
private final WebBindingInitializer bindingInitializer;
/**
* Create {@link DefaultDataBinderFactory} instance.
* @param bindingInitializer a {@link WebBindingInitializer} to initialize new data binder instances with
*/
public DefaultDataBinderFactory(WebBindingInitializer bindingInitializer) {
this.bindingInitializer = bindingInitializer;
}
/**
* Create a new {@link WebDataBinder} for the given target object and initialize it through
* a {@link WebBindingInitializer}.
*/
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
WebDataBinder dataBinder = createBinderInstance(target, objectName);
if (bindingInitializer != null) {
this.bindingInitializer.initBinder(dataBinder, webRequest);
}
return dataBinder;
}
/**
* Create a {@link WebDataBinder} instance.
* @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
*/
protected WebDataBinder createBinderInstance(Object target, String objectName) {
return new WebRequestDataBinder(target, objectName);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2011 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.bind.support;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
/**
* A factory for creating a {@link WebDataBinder} instance for a named target object.
*
* @author Arjen Poutsma
* @since 3.1
*/
public interface WebDataBinderFactory {
/**
* Create a {@link WebDataBinder} for the given object.
* @param webRequest the current request
* @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 the created {@link WebDataBinder} instance, never null
* @throws Exception raised if the creation and initialization of the data binder fails
*/
WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception;
}

View File

@@ -0,0 +1,248 @@
/*
* Copyright 2002-2011 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.method;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Encapsulates information about a bean method consisting of a {@linkplain #getMethod() method} and a
* {@linkplain #getBean() bean}. Provides convenient access to method parameters, the method return value,
* method annotations.
*
* <p>The class may be created with a bean instance or with a bean name (e.g. lazy bean, prototype bean).
* Use {@link #createWithResolvedBean()} to obtain an {@link HandlerMethod} instance with a bean instance
* initialized through the bean factory.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
public class HandlerMethod {
/** Logger that is available to subclasses */
protected final Log logger = LogFactory.getLog(this.getClass());
private final Object bean;
private final Method method;
private final BeanFactory beanFactory;
private MethodParameter[] parameters;
private final Method bridgedMethod;
/**
* Constructs a new handler method with the given bean instance and method.
* @param bean the object bean
* @param method the method
*/
public HandlerMethod(Object bean, Method method) {
Assert.notNull(bean, "bean must not be null");
Assert.notNull(method, "method must not be null");
this.bean = bean;
this.beanFactory = null;
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
}
/**
* Constructs a new handler method with the given bean instance, method name and parameters.
* @param bean the object bean
* @param methodName the method name
* @param parameterTypes the method parameter types
* @throws NoSuchMethodException when the method cannot be found
*/
public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
Assert.notNull(bean, "bean must not be null");
Assert.notNull(methodName, "method must not be null");
this.bean = bean;
this.beanFactory = null;
this.method = bean.getClass().getMethod(methodName, parameterTypes);
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
}
/**
* Constructs a new handler method with the given bean name and method. The bean name will be lazily
* initialized when {@link #createWithResolvedBean()} is called.
* @param beanName the bean name
* @param beanFactory the bean factory to use for bean initialization
* @param method the method for the bean
*/
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
Assert.hasText(beanName, "'beanName' must not be null");
Assert.notNull(beanFactory, "'beanFactory' must not be null");
Assert.notNull(method, "'method' must not be null");
Assert.isTrue(beanFactory.containsBean(beanName),
"Bean factory [" + beanFactory + "] does not contain bean " + "with name [" + beanName + "]");
this.bean = beanName;
this.beanFactory = beanFactory;
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
}
/**
* Returns the bean for this handler method.
*/
public Object getBean() {
return this.bean;
}
/**
* Returns the method for this handler method.
*/
public Method getMethod() {
return this.method;
}
/**
* Returns the type of the handler for this handler method.
* Note that if the bean type is a CGLIB-generated class, the original, user-defined class is returned.
*/
public Class<?> getBeanType() {
if (bean instanceof String) {
String beanName = (String) bean;
return beanFactory.getType(beanName);
}
else {
return ClassUtils.getUserClass(bean.getClass());
}
}
/**
* If the bean method is a bridge method, this method returns the bridged (user-defined) method.
* Otherwise it returns the same method as {@link #getMethod()}.
*/
protected Method getBridgedMethod() {
return bridgedMethod;
}
/**
* Returns the method parameters for this handler method.
*/
public MethodParameter[] getMethodParameters() {
if (this.parameters == null) {
int parameterCount = this.bridgedMethod.getParameterTypes().length;
MethodParameter[] p = new MethodParameter[parameterCount];
for (int i = 0; i < parameterCount; i++) {
p[i] = new HandlerMethodParameter(this.bridgedMethod, i);
}
this.parameters = p;
}
return parameters;
}
/**
* Returns the method return type, as {@code MethodParameter}.
*/
public MethodParameter getReturnType() {
return new HandlerMethodParameter(this.bridgedMethod, -1);
}
/**
* Returns {@code true} if the method return type is void, {@code false} otherwise.
*/
public boolean isVoid() {
return Void.TYPE.equals(getReturnType().getParameterType());
}
/**
* Returns a single annotation on the underlying method traversing its super methods if no
* annotation can be found on the given method itself.
* @param annotationType the type of annotation to introspect the method for.
* @return the annotation, or {@code null} if none found
*/
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
return AnnotationUtils.findAnnotation(this.method, annotationType);
}
/**
* If the provided instance contains a bean name rather than an object instance, the bean name is resolved
* before a {@link HandlerMethod} is created and returned.
*/
public HandlerMethod createWithResolvedBean() {
Object handler = this.bean;
if (this.bean instanceof String) {
String beanName = (String) this.bean;
handler = this.beanFactory.getBean(beanName);
}
return new HandlerMethod(handler, method);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o != null && o instanceof HandlerMethod) {
HandlerMethod other = (HandlerMethod) o;
return this.bean.equals(other.bean) && this.method.equals(other.method);
}
return false;
}
@Override
public int hashCode() {
return 31 * this.bean.hashCode() + this.method.hashCode();
}
@Override
public String toString() {
return method.toGenericString();
}
/**
* A {@link MethodParameter} that resolves method annotations even when the actual annotations
* are on a bridge method rather than on the current method. Annotations on super types are
* also returned via {@link AnnotationUtils#findAnnotation(Method, Class)}.
*/
private class HandlerMethodParameter extends MethodParameter {
public HandlerMethodParameter(Method method, int parameterIndex) {
super(method, parameterIndex);
}
/**
* Return {@link HandlerMethod#getBeanType()} rather than the method's class, which could be
* important for the proper discovery of generic types.
*/
@Override
public Class<?> getDeclaringClass() {
return HandlerMethod.this.getBeanType();
}
/**
* Return the method annotation via {@link HandlerMethod#getMethodAnnotation(Class)}, which will find
* the annotation by traversing super-types and handling annotations on bridge methods correctly.
*/
@Override
public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
return HandlerMethod.this.getMethodAnnotation(annotationType);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2011 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.method;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
/**
* Defines the algorithm for searching handler methods exhaustively including interfaces and parent
* classes while also dealing with parameterized methods and interface and class-based proxies.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public abstract class HandlerMethodSelector {
/**
* Selects handler methods for the given handler type. Callers of this method define handler methods
* of interest through the {@link MethodFilter} parameter.
*
* @param handlerType the handler type to search handler methods on
* @param handlerMethodFilter a {@link MethodFilter} to help recognize handler methods of interest
*/
public static Set<Method> selectMethods(final Class<?> handlerType, final MethodFilter handlerMethodFilter) {
final Set<Method> handlerMethods = new LinkedHashSet<Method>();
Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
Class<?> specificHandlerType = null;
if (!Proxy.isProxyClass(handlerType)) {
handlerTypes.add(handlerType);
specificHandlerType = handlerType;
}
handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces()));
for (Class<?> currentHandlerType : handlerTypes) {
final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
public void doWith(Method method) {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
if (handlerMethodFilter.matches(specificMethod) &&
(bridgedMethod == specificMethod || !handlerMethodFilter.matches(bridgedMethod))) {
handlerMethods.add(specificMethod);
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
}
return handlerMethods;
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2002-2011 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.method.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* Extracts and stores method-to-exception type mappings from a set of {@link ExceptionHandler}-annotated methods.
* Subsequently {@link #getMethod(Exception)} can be used to matches an {@link Exception} to a method.
*
* <p>Method-to-exception type mappings are usually derived from a method's {@link ExceptionHandler} annotation value.
* The method argument list may also be checked for {@link Throwable} types if that's empty. Exception types can be
* mapped to one method only.
*
* <p>When multiple exception types match a given exception, the best matching exception type is selected by sorting
* the list of matches with {@link ExceptionDepthComparator}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class ExceptionMethodMapping {
protected static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis");
private final Map<Class<? extends Throwable>, Method> mappedExceptionTypes =
new HashMap<Class<? extends Throwable>, Method>();
private final Map<Class<? extends Throwable>, Method> resolvedExceptionTypes =
new ConcurrentHashMap<Class<? extends Throwable>, Method>();
/**
* Creates an {@link ExceptionMethodMapping} instance from a set of {@link ExceptionHandler} methods.
* <p>While any {@link ExceptionHandler} methods can be provided, it is expected that the exception types
* handled by any one method do not overlap with the exception types handled by any other method.
* If two methods map to the same exception type, an exception is raised.
* @param methods the {@link ExceptionHandler}-annotated methods to add to the mappings
*/
public ExceptionMethodMapping(Set<Method> methods) {
initExceptionMap(methods);
}
/**
* Examines the provided methods and populates mapped exception types.
*/
private void initExceptionMap(Set<Method> methods) {
for (Method method : methods) {
for (Class<? extends Throwable> exceptionType : getMappedExceptionTypes(method)) {
Method prevMethod = mappedExceptionTypes.put(exceptionType, method);
if (prevMethod != null && !prevMethod.equals(method)) {
throw new IllegalStateException(
"Ambiguous exception handler mapped for [" + exceptionType + "]: {" +
prevMethod + ", " + method + "}.");
}
}
}
}
/**
* Derive the list of exception types mapped to the given method in one of the following ways:
* <ol>
* <li>The {@link ExceptionHandler} annotation value
* <li>{@link Throwable} types that appear in the method parameter list
* </ol>
* @param method the method to derive mapped exception types for
* @return the list of exception types the method is mapped to, or an empty list
*/
@SuppressWarnings("unchecked")
protected List<Class<? extends Throwable>> getMappedExceptionTypes(Method method) {
ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
if (annotation.value().length != 0) {
return Arrays.asList(annotation.value());
}
else {
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
for (Class<?> paramType : method.getParameterTypes()) {
if (Throwable.class.isAssignableFrom(paramType)) {
result.add((Class<? extends Throwable>) paramType);
}
}
Assert.notEmpty(result, "No exception types mapped to {" + method + "}");
return result;
}
}
/**
* Get the {@link ExceptionHandler} method that matches the type of the provided {@link Exception}.
* In case of multiple matches, the best match is selected with {@link ExceptionDepthComparator}.
* @param exception the exception to find a matching {@link ExceptionHandler} method for
* @return the mapped method, or {@code null} if none
*/
public Method getMethod(Exception exception) {
Class<? extends Exception> exceptionType = exception.getClass();
Method method = resolvedExceptionTypes.get(exceptionType);
if (method == null) {
method = resolveExceptionType(exceptionType);
resolvedExceptionTypes.put(exceptionType, method);
}
return (method != NO_METHOD_FOUND) ? method : null;
}
/**
* Resolve the given exception type by iterating mapped exception types.
* Uses {@link #getBestMatchingExceptionType(List, Class)} to select the best match.
* @param exceptionType the exception type to resolve
* @return the best matching method, or {@link ExceptionMethodMapping#NO_METHOD_FOUND}
*/
protected final Method resolveExceptionType(Class<? extends Exception> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
for(Class<? extends Throwable> mappedExceptionType : mappedExceptionTypes.keySet()) {
if (mappedExceptionType.isAssignableFrom(exceptionType)) {
matches.add(mappedExceptionType);
}
}
if (matches.isEmpty()) {
return NO_METHOD_FOUND;
}
else {
return mappedExceptionTypes.get(getBestMatchingExceptionType(matches, exceptionType));
}
}
/**
* Select the best match from the given list of exception types.
*/
protected Class<? extends Throwable> getBestMatchingExceptionType(List<Class<? extends Throwable>> exceptionTypes,
Class<? extends Exception> exceptionType) {
Assert.isTrue(exceptionTypes.size() > 0, "No exception types to select from!");
if (exceptionTypes.size() > 1) {
Collections.sort(exceptionTypes, new ExceptionDepthComparator(exceptionType));
}
return exceptionTypes.get(0);
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2002-2011 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.method.annotation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.support.DefaultDataBinderFactory;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.InvocableHandlerMethod;
/**
* A specialization of {@link DefaultDataBinderFactory} that further initializes {@link WebDataBinder} instances
* through the invocation one or more {@link InitBinder} methods.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class InitBinderMethodDataBinderFactory extends DefaultDataBinderFactory {
private final List<InvocableHandlerMethod> initBinderMethods;
/**
* Create an {@code InitBinderMethodDataBinderFactory} instance with the given {@link InitBinder} methods.
* @param initBinderMethods {@link InitBinder} methods to use when initializing new data binder instances
* @param bindingInitializer a {@link WebBindingInitializer} to initialize new data binder instances with
*/
public InitBinderMethodDataBinderFactory(List<InvocableHandlerMethod> initBinderMethods,
WebBindingInitializer bindingInitializer) {
super(bindingInitializer);
this.initBinderMethods = initBinderMethods;
}
/**
* Create a new {@link WebDataBinder} for the given target object and initialize it through the invocation
* of {@link InitBinder} methods. Only {@link InitBinder} annotations that don't specify attributes names
* and {@link InitBinder} annotations that contain the target object name are invoked.
* @see InitBinder#value()
*/
@Override
public WebDataBinder createBinder(NativeWebRequest request, Object target, String objectName) throws Exception {
WebDataBinder dataBinder = super.createBinder(request, target, objectName);
for (InvocableHandlerMethod binderMethod : this.initBinderMethods) {
InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class);
Set<String> attributeNames = new HashSet<String>(Arrays.asList(annot.value()));
if (attributeNames.size() == 0 || attributeNames.contains(objectName)) {
Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);
if (returnValue != null) {
throw new IllegalStateException("InitBinder methods must not have a return value: " + binderMethod);
}
}
}
return dataBinder;
}
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2002-2011 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.method.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanUtils;
import org.springframework.core.Conventions;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.HttpSessionRequiredException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.InvocableHandlerMethod;
/**
* Provides methods to create and update the "implicit" model for a given request.
*
* <p>{@link #createModel(NativeWebRequest, HandlerMethod)} prepares a model for use with
* a {@link RequestMapping} method. The model is populated with handler session attributes as well
* as request attributes obtained by invoking model attribute methods.
*
* <p>{@link #updateAttributes(NativeWebRequest, SessionStatus, ModelMap, ModelMap)} updates
* the model used for the invocation of a {@link RequestMapping} method, adding handler session
* attributes and {@link BindingResult} structures as necessary.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public final class ModelFactory {
private final List<InvocableHandlerMethod> attributeMethods;
private final WebDataBinderFactory binderFactory;
private final SessionAttributesHandler sessionHandler;
/**
* Create a ModelFactory instance with the provided {@link ModelAttribute} methods.
* @param attributeMethods {@link ModelAttribute}-annotated methods to invoke when populating a model
* @param binderFactory the binder factory to use to add {@link BindingResult} instances to the model
* @param sessionHandler a session attributes handler to synch attributes in the model with the session
*/
public ModelFactory(List<InvocableHandlerMethod> attributeMethods,
WebDataBinderFactory binderFactory,
SessionAttributesHandler sessionHandler) {
this.attributeMethods = attributeMethods;
this.binderFactory = binderFactory;
this.sessionHandler = sessionHandler;
}
/**
* Prepare a model for the current request obtaining attributes in the following order:
* <ol>
* <li>Retrieve previously accessed handler session attributes from the session
* <li>Invoke model attribute methods
* <li>Find request-handling method {@link ModelAttribute}-annotated arguments that are handler session attributes
* </ol>
* <p>As a general rule a model attribute is added only once following the above order.
* @param request the current request
* @param requestMethod the request handling method for which the model is needed
* @return the created model
* @throws Exception if an exception occurs while invoking model attribute methods
*/
public ModelMap createModel(NativeWebRequest request, HandlerMethod requestMethod) throws Exception {
ExtendedModelMap model = new BindingAwareModelMap();
Map<String, ?> sessionAttributes = this.sessionHandler.retrieveHandlerSessionAttributes(request);
model.addAllAttributes(sessionAttributes);
invokeAttributeMethods(request, model);
addSessionAttributesByName(request, requestMethod, model);
return model;
}
/**
* Populate the model by invoking model attribute methods. If two methods provide the same attribute,
* the attribute produced by the first method is used.
*/
private void invokeAttributeMethods(NativeWebRequest request, ExtendedModelMap model) throws Exception {
for (InvocableHandlerMethod attrMethod : this.attributeMethods) {
String modelName = attrMethod.getMethodAnnotation(ModelAttribute.class).value();
if (StringUtils.hasText(modelName) && model.containsAttribute(modelName)) {
continue;
}
Object returnValue = attrMethod.invokeForRequest(request, model);
if (!attrMethod.isVoid()){
String valueName = getNameForReturnValue(returnValue, attrMethod.getReturnType());
if (!model.containsAttribute(valueName)) {
model.addAttribute(valueName, returnValue);
}
}
}
}
/**
* Derive the model name for the given method return value using one of the following:
* <ol>
* <li>The method {@link ModelAttribute} annotation value
* <li>The name of the return type
* <li>The name of the return value type if the method return type is {@code Object}
* </ol>
* @param returnValue the value returned from a method invocation
* @param returnType the return type of the method
* @return the model name, never {@code null} nor empty
*/
public static String getNameForReturnValue(Object returnValue, MethodParameter returnType) {
ModelAttribute annot = returnType.getMethodAnnotation(ModelAttribute.class);
if (annot != null && StringUtils.hasText(annot.value())) {
return annot.value();
}
else {
Method method = returnType.getMethod();
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, returnType.getDeclaringClass());
return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
}
}
/**
* Find request-handling method, {@link ModelAttribute}-annotated arguments that are handler session attributes
* and add them to the model if not present.
*/
private void addSessionAttributesByName(NativeWebRequest request, HandlerMethod requestMethod, ModelMap model) {
for (MethodParameter parameter : requestMethod.getMethodParameters()) {
if (!parameter.hasParameterAnnotation(ModelAttribute.class)) {
continue;
}
String attrName = getNameForParameter(parameter);
if (!model.containsKey(attrName)) {
if (sessionHandler.isHandlerSessionAttribute(attrName, parameter.getParameterType())) {
Object attrValue = sessionHandler.retrieveAttribute(request, attrName);
if (attrValue == null){
new HttpSessionRequiredException("Session attribute '" + attrName + "' not found in session");
}
model.addAttribute(attrName, attrValue);
}
}
}
}
/**
* Derives the model name for the given method parameter using one of the following:
* <ol>
* <li>The parameter {@link ModelAttribute} annotation value
* <li>The name of the parameter type
* </ol>
* @return the method parameter model name, never {@code null} or an empty string
*/
public static String getNameForParameter(MethodParameter parameter) {
ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
String attrName = (annot != null) ? annot.value() : null;
return StringUtils.hasText(attrName) ? attrName : Conventions.getVariableNameForParameter(parameter);
}
/**
* Clean up handler session attributes when {@link SessionStatus#isComplete()} is {@code true}.
* Promote model attributes to the session. Add {@link BindingResult} attributes where missing.
* @param request the current request
* @param sessionStatus indicates whether handler session attributes are to be cleaned
* @param actualModel the model returned from the request method, or {@code null} when the response was handled
* @param implicitModel the model for the request
* @throws Exception if the process of creating {@link BindingResult} attributes causes a problem
*/
public void updateAttributes(NativeWebRequest request,
SessionStatus sessionStatus,
ModelMap actualModel,
ModelMap implicitModel) throws Exception {
if (sessionStatus.isComplete()){
this.sessionHandler.cleanupHandlerSessionAttributes(request);
}
if (actualModel != null) {
this.sessionHandler.storeHandlerSessionAttributes(request, actualModel);
updateBindingResult(request, actualModel);
}
else {
this.sessionHandler.storeHandlerSessionAttributes(request, implicitModel);
}
}
/**
* Add {@link BindingResult} structures to the model for attributes that require it.
*/
private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception {
List<String> keyNames = new ArrayList<String>(model.keySet());
for (String name : keyNames) {
Object value = model.get(name);
if (isBindingCandidate(name, value)) {
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name;
if (!model.containsAttribute(bindingResultKey)) {
WebDataBinder dataBinder = binderFactory.createBinder(request, value, name);
model.put(bindingResultKey, dataBinder.getBindingResult());
}
}
}
}
/**
* Whether the given attribute requires a {@link BindingResult} added to the model.
*/
private boolean isBindingCandidate(String attributeName, Object value) {
if (attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
return false;
}
Class<?> attrType = (value != null) ? value.getClass() : null;
if (this.sessionHandler.isHandlerSessionAttribute(attributeName, attrType)) {
return true;
}
return (value != null && !value.getClass().isArray() && !(value instanceof Collection) &&
!(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass()));
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2002-2011 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.method.annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.context.request.WebRequest;
/**
* Provides operations for managing handler-specific session attributes as defined by the
* {@link SessionAttributes} type-level annotation performing all operations through an
* instance of a {@link SessionAttributeStore}.
*
* <p>A typical scenario involves a handler adding attributes to the {@link Model} during
* a request. At the end of the request, model attributes that match to session attribute
* names defined through an {@link SessionAttributes} annotation are automatically
* "promoted" to the session. Handler session attributes are then removed when
* {@link SessionStatus#setComplete()} is called by a handler.
*
* <p>Therefore "session attributes" for this class means only attributes that have been
* previously confirmed by calls to {@link #isHandlerSessionAttribute(String, Class)}.
* Attribute names that have never been resolved that way will be filtered out from
* operations of this class. That means initially the actual set of resolved session
* attribute names is empty and it grows gradually as attributes are added to
* the {@link Model} and then considered for being added to the session.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class SessionAttributesHandler {
private final Set<String> attributeNames = new HashSet<String>();
@SuppressWarnings("rawtypes")
private final Set<Class> attributeTypes = new HashSet<Class>();
private final Set<String> resolvedAttributeNames = Collections.synchronizedSet(new HashSet<String>(4));
private final SessionAttributeStore attributeStore;
/**
* Creates a {@link SessionAttributesHandler} instance for the specified handlerType.
* <p>Inspects the given handler type for the presence of a {@link SessionAttributes} annotation and
* stores that information for use in subsequent calls to {@link #isHandlerSessionAttribute(String, Class)}.
* If the handler type does not contain such an annotation,
* {@link #isHandlerSessionAttribute(String, Class)} always returns {@code false} and all other operations
* on handler session attributes have no effect on the backend session.
* <p>Use {@link #hasSessionAttributes()} to check if the handler type has defined any session attribute names
* of interest through a {@link SessionAttributes} annotation.
* @param handlerType the handler type to inspect for a {@link SessionAttributes} annotation
* @param attributeStore the {@link SessionAttributeStore} to delegate to for the actual backend session access
*/
public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore attributeStore) {
Assert.notNull(attributeStore, "SessionAttributeStore may not be null.");
this.attributeStore = attributeStore;
SessionAttributes annotation = AnnotationUtils.findAnnotation(handlerType, SessionAttributes.class);
if (annotation != null) {
this.attributeNames.addAll(Arrays.asList(annotation.value()));
this.attributeTypes.addAll(Arrays.asList(annotation.types()));
}
}
/**
* Returns true if the handler type has specified any session attribute names of interest through a
* {@link SessionAttributes} annotation.
*/
public boolean hasSessionAttributes() {
return ((this.attributeNames.size() > 0) || (this.attributeTypes.size() > 0));
}
/**
* Indicate whether or not an attribute is a handler session attribute of interest as defined
* in a {@link SessionAttributes} annotation. Attributes names successfully resolved through
* this method are remembered and in other operations.
* @param attributeName the attribute name to check, must not be null
* @param attributeType the type for the attribute, not required but should be provided when
* available as session attributes of interest can be matched by type
*/
public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) {
Assert.notNull(attributeName, "Attribute name must not be null");
if (this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType)) {
this.resolvedAttributeNames.add(attributeName);
return true;
}
else {
return false;
}
}
/**
* Retrieves the specified attribute through the underlying {@link SessionAttributeStore}.
* Although not required use of this method implies a prior call to
* {@link #isHandlerSessionAttribute(String, Class)} has been made to see if the attribute
* name is a handler-specific session attribute of interest.
* @param request the request for the session operation
* @param attributeName the name of the attribute
* @return the attribute value or {@code null} if none
*/
public Object retrieveAttribute(WebRequest request, String attributeName) {
return this.attributeStore.retrieveAttribute(request, attributeName);
}
/**
* Retrieve attributes for the underlying handler type from the backend session.
* <p>Only attributes that have previously been successfully resolved via calls to
* {@link #isHandlerSessionAttribute(String, Class)} are considered.
* @param request the current request
* @return a map with attributes or an empty map
*/
public Map<String, ?> retrieveHandlerSessionAttributes(WebRequest request) {
Map<String, Object> attributes = new HashMap<String, Object>();
for (String name : this.resolvedAttributeNames) {
Object value = this.attributeStore.retrieveAttribute(request, name);
if (value != null) {
attributes.put(name, value);
}
}
return attributes;
}
/**
* Clean up attributes for the underlying handler type from the backend session.
* <p>Only attributes that have previously been successfully resolved via calls to
* {@link #isHandlerSessionAttribute(String, Class)} are removed.
* @param request the current request
*/
public void cleanupHandlerSessionAttributes(WebRequest request) {
for (String attributeName : this.resolvedAttributeNames) {
this.attributeStore.cleanupAttribute(request, attributeName);
}
}
/**
* Store attributes in the backend session.
* <p>Only attributes that have previously been successfully resolved via calls to
* {@link #isHandlerSessionAttribute(String, Class)} are stored. All other attributes
* from the input map are ignored.
* @param request the current request
* @param attributes the attribute pairs to consider for storing
*/
public void storeHandlerSessionAttributes(WebRequest request, Map<String, Object> attributes) {
for (String name : attributes.keySet()) {
Object value = attributes.get(name);
Class<?> attrType = (value != null) ? value.getClass() : null;
if (isHandlerSessionAttribute(name, attrType)) {
this.attributeStore.storeAttribute(request, name, value);
}
}
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException;
import org.springframework.beans.factory.config.BeanExpressionContext;
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.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestScope;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Abstract base class for argument resolvers that resolve named values.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final ConfigurableBeanFactory beanFactory;
private final BeanExpressionContext expressionContext;
private Map<MethodParameter, NamedValueInfo> namedValueInfoCache =
new ConcurrentHashMap<MethodParameter, NamedValueInfo>();
public AbstractNamedValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
this.expressionContext = (beanFactory != null) ? new BeanExpressionContext(beanFactory, new RequestScope()) : null;
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
public final Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
Object arg = resolveNamedValueArgument(webRequest, parameter, namedValueInfo.name);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveDefaultValue(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required) {
handleMissingValue(namedValueInfo.name, parameter);
}
arg = checkForNull(namedValueInfo.name, arg, paramType);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
return binder.convertIfNecessary(arg, paramType, parameter);
}
else {
return arg;
}
}
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo result = namedValueInfoCache.get(parameter);
if (result == null) {
NamedValueInfo info = createNamedValueInfo(parameter);
String name = info.name;
if (name.length() == 0) {
name = parameter.getParameterName();
if (name == null) {
throw new IllegalStateException("No parameter name specified for argument of type [" +
parameter.getParameterType().getName() +
"], and no parameter name information found in class file either.");
}
}
boolean required = info.required;
String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
result = new NamedValueInfo(name, required, defaultValue);
namedValueInfoCache.put(parameter, result);
}
return result;
}
/**
* Creates a new {@link NamedValueInfo} object for the given method parameter.
*
* <p>Implementations typically retrieve the method annotation by means of {@link
* MethodParameter#getParameterAnnotation(Class)}.
*
* @param parameter the method parameter
* @return the named value information
*/
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
/**
* Resolves the given parameter into a method argument.
*
* @param webRequest the current web request, allowing access to the native request as well
* @param parameter the parameter to resolve to an argument. This parameter must have previously been passed to the
* {@link #supportsParameter(org.springframework.core.MethodParameter)} method of this interface, which must have
* returned {@code true}.
* @param name the name
* @return the resolved argument. May be {@code null}.
* @throws Exception in case of errors
*/
protected abstract Object resolveNamedValueArgument(NativeWebRequest webRequest,
MethodParameter parameter,
String name) throws Exception;
private Object resolveDefaultValue(String value) {
if (beanFactory == null) {
return value;
}
String placeholdersResolved = beanFactory.resolveEmbeddedValue(value);
BeanExpressionResolver exprResolver = beanFactory.getBeanExpressionResolver();
if (exprResolver == null) {
return value;
}
return exprResolver.evaluate(placeholdersResolved, expressionContext);
}
/**
* Invoked when a named value is required, but
* {@link #resolveNamedValueArgument(NativeWebRequest, MethodParameter, String)} returned {@code null}
* and there is no default value set.
*
* <p>Concrete subclasses typically throw an exception in this scenario.
*
* @param name the name
* @param parameter the method parameter
*/
protected abstract void handleMissingValue(String name, MethodParameter parameter) throws ServletException;
private Object checkForNull(String name, Object value, Class<?> paramType) {
if (value == null) {
if (Boolean.TYPE.equals(paramType)) {
return Boolean.FALSE;
}
else if (paramType.isPrimitive()) {
throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
"' is present but cannot be translated into a null value due to being declared as a " +
"primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
}
}
return value;
}
/**
* Represents the information about a named value, including name, whether it's required and a default value.
*/
protected static class NamedValueInfo {
private final String name;
private final boolean required;
private final String defaultValue;
protected NamedValueInfo(String name, boolean required, String defaultValue) {
this.name = name;
this.required = required;
this.defaultValue = defaultValue;
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.util.UrlPathHelper;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that supports arguments annotated with
* {@link CookieValue @CookieValue}.
*
* @author Arjen Poutsma
*/
public class CookieValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
public CookieValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory) {
super(beanFactory);
}
public UrlPathHelper getUrlPathHelper() {
return urlPathHelper;
}
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
this.urlPathHelper = urlPathHelper;
}
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CookieValue.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
CookieValue annotation = parameter.getParameterAnnotation(CookieValue.class);
return new CookieValueNamedValueInfo(annotation);
}
@Override
protected Object resolveNamedValueArgument(NativeWebRequest webRequest,
MethodParameter parameter,
String cookieName) throws Exception {
throw new UnsupportedOperationException("@CookieValue not supported");
}
@Override
protected void handleMissingValue(String cookieName, MethodParameter parameter) {
throw new IllegalStateException(
"Missing cookie value '" + cookieName + "' of type [" + parameter.getParameterType().getName() + "]");
}
private static class CookieValueNamedValueInfo extends NamedValueInfo {
private CookieValueNamedValueInfo(CookieValue annotation) {
super(annotation.value(), annotation.required(), annotation.defaultValue());
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* An implementation of {@link HandlerMethodArgumentResolver} that resolves {@link Errors} method parameters.
* Such parameters must be preceded by {@link ModelAttribute} parameters as described in {@link RequestMapping}.
*
* @author Rossen Stoyanchev
*/
public class ErrorsMethodArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return Errors.class.isAssignableFrom(paramType);
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
public Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
if (model.size() > 0) {
List<String> keys = new ArrayList<String>(model.keySet());
String lastKey = keys.get(model.size()-1);
if (isBindingResultKey(lastKey)) {
return model.get(lastKey);
}
}
throw new IllegalStateException("Errors/BindingResult argument declared "
+ "without preceding model attribute. Check your handler method signature!");
}
private boolean isBindingResultKey(String key) {
return key.startsWith(BindingResult.MODEL_KEY_PREFIX);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import javax.servlet.ServletException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that supports arguments annotated
* with {@link Value @Value}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
public ExpressionValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory) {
super(beanFactory);
}
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Value.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
Value annotation = parameter.getParameterAnnotation(Value.class);
return new ExpressionValueNamedValueInfo(annotation);
}
@Override
protected Object resolveNamedValueArgument(NativeWebRequest webRequest, MethodParameter parameter, String name)
throws Exception {
// Only interested in default value resolution
return null;
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
// Should not happen
throw new UnsupportedOperationException();
}
private static class ExpressionValueNamedValueInfo extends NamedValueInfo {
private ExpressionValueNamedValueInfo(Value annotation) {
super("@Value", false, annotation.value());
}
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.lang.annotation.Annotation;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.bind.support.WebRequestDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Resolves model attribute method parameters.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class ModelAttributeMethodProcessor
implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
private final boolean resolveArgumentsWithoutAnnotations;
/**
* Creates a {@link ModelAttributeMethodProcessor} instance.
* @param resolveArgumentsWithoutAnnotations enable default resolution mode in which arguments without
* annotations that aren't simple types (see {@link BeanUtils#isSimpleProperty(Class)})
* are also treated as model attributes with a default name based on the model attribute type.
*/
public ModelAttributeMethodProcessor(boolean resolveArgumentsWithoutAnnotations) {
this.resolveArgumentsWithoutAnnotations = resolveArgumentsWithoutAnnotations;
}
/**
* @return true if the parameter is annotated with {@link ModelAttribute} or if it is a
* simple type without any annotations.
*/
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
return true;
}
else if (this.resolveArgumentsWithoutAnnotations && !parameter.hasParameterAnnotations()) {
return !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
else {
return false;
}
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
/**
* Creates a {@link WebDataBinder} for the target model attribute and applies data binding to it.
* The model attribute may be obtained from the "implicit" model, from the session via, or by
* direct instantiation.
*
* @throws Exception if invoking an data binder initialization fails or if data binding and/or
* validation results in errors and the next method parameter is not of type {@link Errors}.
*/
public final Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
WebDataBinder binder = createDataBinder(parameter, model, webRequest, binderFactory);
if (binder.getTarget() != null) {
doBind(binder, webRequest);
if (shouldValidate(parameter)) {
binder.validate();
}
if (failOnError(parameter) && binder.getBindingResult().hasErrors()) {
throw new BindException(binder.getBindingResult());
}
}
model.putAll(binder.getBindingResult().getModel());
return binder.getTarget();
}
/**
* Creates a {@link WebDataBinder} for a target object which may be obtained from the "implicit" model,
* the session via {@link SessionAttributeStore}, or by direct instantiation.
*/
private WebDataBinder createDataBinder(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
String attrName = ModelFactory.getNameForParameter(parameter);
Object target;
if (model.containsKey(attrName)) {
target = model.get(attrName);
}
else {
target = BeanUtils.instantiateClass(parameter.getParameterType());
}
return binderFactory.createBinder(webRequest, target, attrName);
}
protected void doBind(WebDataBinder binder, NativeWebRequest request) {
((WebRequestDataBinder) binder).bind(request);
}
/**
* @return true if {@link DataBinder#validate()} should be invoked, false otherwise.
*/
protected boolean shouldValidate(MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if ("Valid".equals(annot.annotationType().getSimpleName())) {
return true;
}
}
return false;
}
/**
* @return true if the binding or validation errors should result in a {@link BindException}, false otherwise.
*/
protected boolean failOnError(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;
}
public boolean supportsReturnType(MethodParameter returnType) {
return returnType.getMethodAnnotation(ModelAttribute.class) != null;
}
public <V> void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer<V> mavContainer,
NativeWebRequest webRequest) throws Exception {
String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
mavContainer.addModelAttribute(name, returnValue);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Resolves {@link Model} and {@link Map} method parameters.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType);
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
public Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
if (Model.class.isAssignableFrom(paramType)) {
return model;
}
else if (Map.class.isAssignableFrom(paramType)) {
return model;
}
// should not happen
throw new UnsupportedOperationException();
}
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
boolean hasModelAttr = returnType.getMethodAnnotation(ModelAttribute.class) != null;
return (Model.class.isAssignableFrom(paramType)
|| (Map.class.isAssignableFrom(paramType) && !hasModelAttr));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public <V> void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer<V> mavContainer,
NativeWebRequest webRequest) throws Exception {
if (returnValue instanceof Model) {
mavContainer.addModelAttributes((Model) returnValue);
}
else {
mavContainer.addModelAttributes((Map) returnValue);
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.ui.ModelMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that supports {@link Map} arguments annotated with
* {@link RequestHeader @RequestHeader}.
*
* @author Arjen Poutsma
*/
public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestHeader.class)
&& Map.class.isAssignableFrom(parameter.getParameterType());
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
public Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
if (MultiValueMap.class.isAssignableFrom(paramType)) {
MultiValueMap<String, String> result;
if (HttpHeaders.class.isAssignableFrom(paramType)) {
result = new HttpHeaders();
}
else {
result = new LinkedMultiValueMap<String, String>();
}
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
for (String headerValue : webRequest.getHeaderValues(headerName)) {
result.add(headerName, headerValue);
}
}
return result;
}
else {
Map<String, String> result = new LinkedHashMap<String, String>();
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String headerValue = webRequest.getHeader(headerName);
result.put(headerName, headerValue);
}
return result;
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.Map;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that supports arguments annotated with
* {@link RequestHeader @RequestHeader}.
*
* @author Arjen Poutsma
*/
public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
public RequestHeaderMethodArgumentResolver(ConfigurableBeanFactory beanFactory) {
super(beanFactory);
}
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestHeader.class)
&& !Map.class.isAssignableFrom(parameter.getParameterType());
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestHeader annotation = parameter.getParameterAnnotation(RequestHeader.class);
return new RequestHeaderNamedValueInfo(annotation);
}
@Override
protected Object resolveNamedValueArgument(NativeWebRequest webRequest,
MethodParameter parameter,
String headerName) throws Exception {
String[] headerValues = webRequest.getHeaderValues(headerName);
if (headerValues != null) {
return (headerValues.length == 1 ? headerValues[0] : headerValues);
}
else {
return null;
}
}
@Override
protected void handleMissingValue(String headerName, MethodParameter parameter) {
throw new IllegalStateException(
"Missing header '" + headerName + "' of type [" + parameter.getParameterType().getName() + "]");
}
private static class RequestHeaderNamedValueInfo extends NamedValueInfo {
private RequestHeaderNamedValueInfo(RequestHeader annotation) {
super(annotation.value(), annotation.required(), annotation.defaultValue());
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that supports {@link Map} arguments annotated with
* {@link RequestParam @RequestParam}.
*
* @author Arjen Poutsma
*/
public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
RequestParam requestParamAnnot = parameter.getParameterAnnotation(RequestParam.class);
if (requestParamAnnot != null) {
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
return !StringUtils.hasText(requestParamAnnot.value());
}
}
return false;
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
public Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
Map<String, String[]> parameterMap = webRequest.getParameterMap();
if (MultiValueMap.class.isAssignableFrom(paramType)) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(parameterMap.size());
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
for (String value : entry.getValue()) {
result.add(entry.getKey(), value);
}
}
return result;
}
else {
Map<String, String> result = new LinkedHashMap<String, String>(parameterMap.size());
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
if (entry.getValue().length > 0) {
result.put(entry.getKey(), entry.getValue()[0]);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that supports arguments annotated with
* {@link RequestParam @RequestParam}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
private final boolean resolveParamsWithoutAnnotations;
/**
* Creates a {@link RequestParamMethodArgumentResolver} instance.
*
* @param beanFactory the bean factory to use for resolving default value expressions
* @param resolveParamsWithoutAnnotations enable default resolution mode in which parameters without
* annotations that are simple types (see {@link BeanUtils#isSimpleProperty(Class)})
* are also treated as model attributes with a default name based on the method argument name.
*/
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
boolean resolveParamsWithoutAnnotations) {
super(beanFactory);
this.resolveParamsWithoutAnnotations = resolveParamsWithoutAnnotations;
}
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
RequestParam requestParamAnnot = parameter.getParameterAnnotation(RequestParam.class);
if (requestParamAnnot != null) {
if (Map.class.isAssignableFrom(paramType)) {
return StringUtils.hasText(requestParamAnnot.value());
}
return true;
}
else if (this.resolveParamsWithoutAnnotations && !parameter.hasParameterAnnotations()) {
return BeanUtils.isSimpleProperty(paramType);
}
else {
return false;
}
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
return (annotation != null) ?
new RequestParamNamedValueInfo(annotation) :
new RequestParamNamedValueInfo();
}
@Override
protected Object resolveNamedValueArgument(NativeWebRequest webRequest,
MethodParameter parameter,
String paramName) throws Exception {
MultipartRequest multipartRequest = webRequest.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(paramName);
if (!files.isEmpty()) {
return (files.size() == 1 ? files.get(0) : files);
}
}
String[] paramValues = webRequest.getParameterValues(paramName);
if (paramValues != null) {
return paramValues.length == 1 ? paramValues[0] : paramValues;
}
else {
return null;
}
}
@Override
protected void handleMissingValue(String paramName, MethodParameter parameter) throws ServletException {
throw new MissingServletRequestParameterException(paramName, parameter.getParameterType().getSimpleName());
}
private class RequestParamNamedValueInfo extends NamedValueInfo {
private RequestParamNamedValueInfo() {
super("", true, ValueConstants.DEFAULT_NONE);
}
private RequestParamNamedValueInfo(RequestParam annotation) {
super(annotation.value(), annotation.required(), annotation.defaultValue());
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-2011 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.method.annotation.support;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Adapts a {@link WebArgumentResolver} into the {@link HandlerMethodArgumentResolver} contract.
*
* @author Arjen Poutsma
*/
public class WebArgumentResolverAdapter implements HandlerMethodArgumentResolver {
private final WebArgumentResolver adaptee;
public WebArgumentResolverAdapter(WebArgumentResolver adaptee) {
Assert.notNull(adaptee, "'adaptee' must not be null");
this.adaptee = adaptee;
}
public boolean supportsParameter(MethodParameter parameter) {
try {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
Object result ;
if (requestAttributes instanceof NativeWebRequest) {
result = adaptee.resolveArgument(parameter, (NativeWebRequest) requestAttributes);
}
else {
result = adaptee.resolveArgument(parameter, null);
}
if (result == WebArgumentResolver.UNRESOLVED) {
return false;
}
else {
return ClassUtils.isAssignableValue(parameter.getParameterType(), result);
}
}
catch (Exception ex) {
// ignore
return false;
}
}
public boolean usesResponseArgument(MethodParameter parameter) {
return false;
}
public Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
Object result = adaptee.resolveArgument(parameter, webRequest);
if (result == WebArgumentResolver.UNRESOLVED || !ClassUtils.isAssignableValue(paramType, result)) {
throw new IllegalStateException(
"Standard argument type [" + paramType.getName() + "] resolved to incompatible value of type [" +
(result != null ? result.getClass() : null) +
"]. Consider declaring the argument type in a less specific fashion.");
}
return result;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2002-2011 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.method.support;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Strategy interface for resolving method parameters into argument values from a given request.
*
* @author Arjen Poutsma
* @since 3.1
*/
public interface HandlerMethodArgumentResolver extends HandlerMethodProcessor {
/**
* Indicates whether the given {@linkplain MethodParameter method parameter} is supported by this resolver.
*
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter; {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request and a {@link ModelMap} providing
* the ability to both access and add new model attributes. A {@link WebDataBinderFactory} is also provided
* for creating a {@link WebDataBinder} instance to use for data binding and type conversion.
*
* @param parameter the parameter to resolve to an argument. This parameter must have previously been passed to
* {@link #supportsParameter(org.springframework.core.MethodParameter)} and it must have returned {@code true}
* @param model the model for the current request
* @param webRequest the current request.
* @param binderFactory a factory in case the resolver needs to create a {@link WebDataBinder} instance
* @return the resolved argument value, or {@code null}.
* @throws Exception in case of errors with the preparation of argument values
*/
Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception;
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2002-2011 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.method.support;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Implementation of {@link HandlerMethodArgumentResolver} that resolves handler method arguments by delegating
* to a list of registered {@link HandlerMethodArgumentResolver}s.
*
* <p>Previously resolved method argument types are cached internally for faster lookups.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class HandlerMethodArgumentResolverContainer implements HandlerMethodArgumentResolver {
protected final Log logger = LogFactory.getLog(HandlerMethodArgumentResolverContainer.class);
private List<HandlerMethodArgumentResolver> argumentResolvers =
new ArrayList<HandlerMethodArgumentResolver>();
private Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
new ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver>();
/**
* Indicates whether the given {@linkplain MethodParameter method parameter} is supported by any of the
* registered {@link HandlerMethodArgumentResolver}s.
*/
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
/**
* Resolve a method parameter into an argument value for the given request by iterating over registered
* {@link HandlerMethodArgumentResolver}s to find one that supports the given method parameter.
*/
public Object resolveArgument(MethodParameter parameter,
ModelMap model,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver != null) {
return resolver.resolveArgument(parameter, model, webRequest, binderFactory);
}
throw new IllegalStateException(
"No suitable HandlerMethodArgumentResolver found. " +
"supportsParameter(MethodParameter) should have been called previously.");
}
/**
* Find a registered {@link HandlerMethodArgumentResolver} that supports the given method parameter.
* @return a {@link HandlerMethodArgumentResolver} instance, or {@code null} if none
*/
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : argumentResolvers) {
if (logger.isTraceEnabled()) {
logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
parameter.getGenericParameterType() + "]");
}
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
/**
* Indicates whether the argument resolver that supports the given method parameter uses the response argument.
* @see HandlerMethodProcessor#usesResponseArgument(MethodParameter)
*/
public boolean usesResponseArgument(MethodParameter parameter) {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
return (resolver != null && resolver.usesResponseArgument(parameter));
}
/**
* Register the given {@link HandlerMethodArgumentResolver}.
*/
public void registerArgumentResolver(HandlerMethodArgumentResolver argumentResolver) {
this.argumentResolvers.add(argumentResolver);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2002-2011 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.method.support;
import org.springframework.core.MethodParameter;
/**
* A base interface for {@link HandlerMethodArgumentResolver}s and {@link HandlerMethodReturnValueHandler}s.
*
* @author Arjen Poutsma
* @since 3.1
*/
public interface HandlerMethodProcessor {
/**
* Indicates whether the given {@linkplain org.springframework.core.MethodParameter method parameter},
* uses the response argument with the implication that the response will be handled directly by invoking
* the method and will not require view name resolution followed by rendering.
*
* @param parameter the method parameter, either a method argument or a return type
* @return {@code true} if the supplied parameter uses the response argument; {@code false} otherwise
*/
boolean usesResponseArgument(MethodParameter parameter);
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2002-2011 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.method.support;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Strategy interface to process the value returned from a handler method invocation.
*
* @author Arjen Poutsma
* @since 3.1
*/
public interface HandlerMethodReturnValueHandler extends HandlerMethodProcessor {
/**
* Indicates whether the given {@linkplain MethodParameter method return type} is supported by this handler.
*
* @param returnType the method return type to check
* @return {@code true} if this handler supports the supplied return type; {@code false} otherwise
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* Handles the given value returned by a handler method invocation by writing directly to the response
* or by using the {@code mavContainer} argument to add model attributes and/or set the view.
*
* @param returnValue the return value to handle
* @param returnType the return type to handle. This type must have previously been passed to
* {@link #supportsReturnType(org.springframework.core.MethodParameter)} and it must have returned {@code true}
* @param mavContainer records model and view choices
* @param webRequest the current request
* @throws Exception in case of errors
*/
<V> void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer<V> mavContainer,
NativeWebRequest webRequest) throws Exception;
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2011 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.method.support;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Implementation of {@link HandlerMethodReturnValueHandler} that handles method return values by
* delegating to a list of registered {@link HandlerMethodReturnValueHandler}s.
*
* <p>Previously resolved return types are cached internally for faster lookups.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class HandlerMethodReturnValueHandlerContainer implements HandlerMethodReturnValueHandler {
protected final Log logger = LogFactory.getLog(HandlerMethodArgumentResolverContainer.class);
private List<HandlerMethodReturnValueHandler> returnValueHandlers =
new ArrayList<HandlerMethodReturnValueHandler>();
private Map<MethodParameter, HandlerMethodReturnValueHandler> returnValueHandlerCache =
new ConcurrentHashMap<MethodParameter, HandlerMethodReturnValueHandler>();
/**
* Indicates whether the given {@linkplain MethodParameter method return type} is supported by any of the
* registered {@link HandlerMethodReturnValueHandler}s.
*/
public boolean supportsReturnType(MethodParameter returnType) {
return getReturnValueHandler(returnType) != null;
}
/**
* Handles the given method return value by iterating over registered {@link HandlerMethodReturnValueHandler}s
* to find one that supports it.
*/
public <V> void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer<V> mavContainer,
NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType);
if (handler != null) {
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
return;
}
throw new IllegalStateException(
"No suitable HandlerMethodReturnValueHandler found. " +
"supportsReturnType(MethodParameter) should have been called previously");
}
/**
* Find a registered {@link HandlerMethodReturnValueHandler} that supports the given method return type.
*/
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
if (this.returnValueHandlers == null) {
return null;
}
HandlerMethodReturnValueHandler result = this.returnValueHandlerCache.get(returnType);
if (result == null) {
for (HandlerMethodReturnValueHandler methodReturnValueHandler : returnValueHandlers) {
if (logger.isTraceEnabled()) {
logger.trace("Testing if return value handler [" + methodReturnValueHandler + "] supports [" +
returnType.getGenericParameterType() + "]");
}
if (methodReturnValueHandler.supportsReturnType(returnType)) {
result = methodReturnValueHandler;
this.returnValueHandlerCache.put(returnType, methodReturnValueHandler);
break;
}
}
}
return result;
}
/**
* Indicates whether the return value handler that supports the given method parameter uses the response.
* @see HandlerMethodProcessor#usesResponseArgument(MethodParameter)
*/
public boolean usesResponseArgument(MethodParameter parameter) {
HandlerMethodReturnValueHandler handler = getReturnValueHandler(parameter);
return (handler != null && handler.usesResponseArgument(parameter));
}
/**
* Register the given {@link HandlerMethodReturnValueHandler}.
*/
public void registerReturnValueHandler(HandlerMethodReturnValueHandler returnValuehandler) {
returnValueHandlers.add(returnValuehandler);
}
}

View File

@@ -0,0 +1,241 @@
/*
* Copyright 2002-2011 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.method.support;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.ui.ModelMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.HandlerMethod;
/**
* Provides a way to invoke a handler method after resolving its method argument values through
* {@link HandlerMethodArgumentResolver}s in the context of the current request.
*
* <p>Resolving argument values often requires a {@link WebDataBinder} for data binding and type conversion.
* Use {@link #setDataBinderFactory(WebDataBinderFactory)} to provide a factory for that. The list of argument
* resolvers can be set via {@link #setArgumentResolverContainer(HandlerMethodArgumentResolverContainer)}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class InvocableHandlerMethod extends HandlerMethod {
private HandlerMethodArgumentResolverContainer argumentResolvers = new HandlerMethodArgumentResolverContainer();
private WebDataBinderFactory dataBinderFactory;
private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
/**
* Constructs a new invocable handler method with the given bean instance and method.
* @param bean the bean instance
* @param method the method
*/
public InvocableHandlerMethod(Object bean, Method method) {
super(bean, method);
}
/**
* Constructs a new invocable handler method with the given bean instance, method name and parameters.
* @param bean the object bean
* @param methodName the method name
* @param parameterTypes the method parameter types
* @throws NoSuchMethodException when the method cannot be found
*/
public InvocableHandlerMethod(
Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
super(bean, methodName, parameterTypes);
}
/**
* Sets the {@link WebDataBinderFactory} to be passed to argument resolvers that require a
* {@link WebDataBinder} to do type conversion or data binding on the method argument value.
*
* @param dataBinderFactory the data binder factory.
*/
public void setDataBinderFactory(WebDataBinderFactory dataBinderFactory) {
this.dataBinderFactory = dataBinderFactory;
}
/**
* Set {@link HandlerMethodArgumentResolver}s to use to use for resolving method argument values.
*/
public void setArgumentResolverContainer(HandlerMethodArgumentResolverContainer argumentResolvers) {
this.argumentResolvers = argumentResolvers;
}
/**
* Set the ParameterNameDiscoverer to use for resolving method parameter names if needed
* (e.g. for default attribute names).
* <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* Invoke the method after resolving its argument values based on the given request.
* <p>Most argument values are resolved with the help of {@link HandlerMethodArgumentResolver}s
* configured via {@link #setArgumentResolverContainer(HandlerMethodArgumentResolverContainer)}.
* However, the {@code provideArgs} parameter can be used to supply argument values for use
* directly rather than relying on argument resolution - e.g. {@link WebDataBinder},
* {@link SessionStatus}, or the thrown exception in a HandlerExceptionResolver.
* @param request the current request
* @param model the model used throughout the current request
* @param providedArgs argument values to try to use, thus bypassing argument resolution
* @return the raw value returned by the invoked method
*/
public final Object invokeForRequest(NativeWebRequest request, ModelMap model, Object... providedArgs)
throws Exception {
Object[] args = getMethodArguments(request, model, providedArgs);
if (logger.isTraceEnabled()) {
StringBuilder builder = new StringBuilder("Invoking [");
builder.append(this.getMethod().getName()).append("] method with arguments ");
builder.append(Arrays.asList(args));
logger.trace(builder.toString());
}
Object returnValue = invoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + this.getMethod().getName() + "] returned [" + returnValue + "]");
}
return returnValue;
}
private Object[] getMethodArguments(NativeWebRequest request, ModelMap model, Object... providedArgs)
throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (this.argumentResolvers.supportsParameter(parameter)) {
args[i] = this.argumentResolvers.resolveArgument(parameter, model, request, dataBinderFactory);
}
else {
throw new IllegalStateException("Cannot resolve argument index=" + parameter.getParameterIndex() + ""
+ ", name=" + parameter.getParameterName() + ", type=" + parameter.getParameterType()
+ " in method " + toString());
}
}
return args;
}
private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) {
if (providedArgs == null) {
return null;
}
for (Object providedArg : providedArgs) {
if (parameter.getParameterType().isInstance(providedArg)) {
return providedArg;
}
}
return null;
}
/**
* Invokes this handler method with the given argument values.
* @param args the argument values
* @return the result of the invocation
* @throws Exception when the method invocation results in an exception
*/
private Object invoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(this.getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
handleIllegalArgumentException(ex, args);
throw ex;
}
catch (InvocationTargetException ex) {
handleInvocationTargetException(ex);
throw new IllegalStateException(
"Unexpected exception thrown by method - " + ex.getTargetException().getClass().getName() + ": " +
ex.getTargetException().getMessage());
}
}
private void handleIllegalArgumentException(IllegalArgumentException ex, Object... args) {
StringBuilder builder = new StringBuilder(ex.getMessage());
builder.append(" :: method=").append(getBridgedMethod().toGenericString());
builder.append(" :: invoked with handler type=").append(getBeanType().getName());
if (args != null && args.length > 0) {
builder.append(" and argument types ");
for (int i = 0; i < args.length; i++) {
builder.append(" : arg[").append(i).append("] ").append(args[i].getClass());
}
}
else {
builder.append(" and 0 arguments");
}
throw new IllegalArgumentException(builder.toString(), ex);
}
private void handleInvocationTargetException(InvocationTargetException ex) throws Exception {
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
if (targetException instanceof Error) {
throw (Error) targetException;
}
if (targetException instanceof Exception) {
throw (Exception) targetException;
}
}
/**
* Whether any of the registered {@link HandlerMethodArgumentResolver}s use the response argument.
* @see HandlerMethodProcessor#usesResponseArgument(MethodParameter)
*/
protected boolean usesResponseArgument() {
MethodParameter[] methodParameters = getMethodParameters();
for (MethodParameter methodParameter : methodParameters) {
if (this.argumentResolvers.usesResponseArgument(methodParameter)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2002-2011 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.method.support;
import java.util.Map;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
/**
* Contains model and view choices made by {@link HandlerMethodReturnValueHandler}s.
*
* <p>Allows return value handlers to set only the bits that are relevant to them - i.e. model, view,
* or none, while also taking care of merging attributes added by the {@link HandlerMethodReturnValueHandler}
* with attributes from the implicit model.
*
* @author Rossen Stoyanchev
* @since 3.1
*
* @param <V> Servlet or Portlet specific View type.
*/
public class ModelAndViewContainer<V> {
private String viewName;
private V view;
private final ModelMap actualModel = new ExtendedModelMap();
private final ModelMap implicitModel;
public ModelAndViewContainer(ModelMap implicitModel) {
this.implicitModel = (implicitModel != null) ? implicitModel : new ExtendedModelMap();
}
public ModelMap getModel() {
return new ExtendedModelMap().addAllAttributes(actualModel).mergeAttributes(implicitModel);
}
public String getViewName() {
return this.viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public V getView() {
return this.view;
}
public void setView(V view) {
this.view = view;
}
public void addModelAttributes(Model attributes) {
actualModel.addAllAttributes(attributes.asMap());
}
public void addModelAttributes(Map<String, Object> attributes) {
actualModel.addAllAttributes(attributes);
}
public void addModelAttribute(String name, Object value) {
actualModel.addAttribute(name, value);
}
}