From acc75aa4b898a34dce42748df5b54624f8b3e9f2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Apr 2011 11:30:59 +0000 Subject: [PATCH] SPR-8215 Move HandlerMethod code into trunk --- ...bstractHandlerMethodExceptionResolver.java | 83 +++ .../handler/AbstractHandlerMethodMapping.java | 284 +++++++++++ .../handler/AbstractUrlHandlerMapping.java | 8 +- .../servlet/handler/MappedInterceptors.java | 50 +- .../method/AbstractHandlerMethodAdapter.java | 112 +++++ .../method/annotation/RequestCondition.java | 29 ++ .../annotation/RequestConditionFactory.java | 285 +++++++++++ .../mvc/method/annotation/RequestKey.java | 326 ++++++++++++ .../RequestMappingHandlerAdapter.java | 473 ++++++++++++++++++ ...MappingHandlerMethodExceptionResolver.java | 222 ++++++++ .../RequestMappingHandlerMethodMapping.java | 320 ++++++++++++ ...vletInitBinderMethodDataBinderFactory.java | 53 ++ .../ServletInvocableHandlerMethod.java | 160 ++++++ ...stractMessageConverterMethodProcessor.java | 146 ++++++ .../DefaultMethodReturnValueHandler.java | 89 ++++ .../support/HttpEntityMethodProcessor.java | 147 ++++++ .../ModelAndViewMethodReturnValueHandler.java | 52 ++ .../PathVariableMethodArgumentResolver.java | 79 +++ .../RequestResponseBodyMethodProcessor.java | 98 ++++ ...vletCookieValueMethodArgumentResolver.java | 56 +++ .../ServletModelAttributeMethodProcessor.java | 54 ++ .../ServletRequestMethodArgumentResolver.java | 91 ++++ ...ServletResponseMethodArgumentResolver.java | 73 +++ .../support/ViewMethodReturnValueHandler.java | 56 +++ .../handler/HandlerMethodMappingTests.java | 134 +++++ ...trollerMethodAnnotationDetectionTests.java | 251 ++++++++++ .../RequestConditionFactoryTests.java | 155 ++++++ .../annotation/RequestKeyComparatorTests.java | 149 ++++++ .../method/annotation/RequestKeyTests.java | 218 ++++++++ ...MappingHandlerAdapterIntegrationTests.java | 349 +++++++++++++ .../RequestMappingHandlerAdapterTests.java | 97 ++++ ...estMappingHandlerMethodDetectionTests.java | 167 +++++++ ...ngHandlerMethodExceptionResolverTests.java | 239 +++++++++ ...questMappingHandlerMethodMappingTests.java | 153 ++++++ .../ServletInvocableHandlerMethodTests.java | 73 +++ .../DefaultMethodReturnValueHandlerTests.java | 81 +++ .../HttpEntityMethodProcessorTests.java | 243 +++++++++ ...thVariableMethodArgumentResolverTests.java | 97 ++++ ...questResponseBodyMethodProcessorTests.java | 190 +++++++ ...ookieValueMethodArgumentResolverTests.java | 117 +++++ ...letRequestMethodArgumentResolverTests.java | 147 ++++++ ...etResponseMethodArgumentResolverTests.java | 95 ++++ .../ViewMethodReturnValueHandlerTests.java | 91 ++++ .../support/DefaultDataBinderFactory.java | 64 +++ .../bind/support/WebDataBinderFactory.java | 40 ++ .../web/method/HandlerMethod.java | 248 +++++++++ .../web/method/HandlerMethodSelector.java | 71 +++ .../annotation/ExceptionMethodMapping.java | 163 ++++++ .../InitBinderMethodDataBinderFactory.java | 79 +++ .../web/method/annotation/ModelFactory.java | 249 +++++++++ .../annotation/SessionAttributesHandler.java | 177 +++++++ ...tractNamedValueMethodArgumentResolver.java | 195 ++++++++ .../CookieValueMethodArgumentResolver.java | 78 +++ .../support/ErrorsMethodArgumentResolver.java | 69 +++ ...ExpressionValueMethodArgumentResolver.java | 70 +++ .../ModelAttributeMethodProcessor.java | 171 +++++++ .../support/ModelMethodProcessor.java | 85 ++++ ...equestHeaderMapMethodArgumentResolver.java | 82 +++ .../RequestHeaderMethodArgumentResolver.java | 76 +++ ...RequestParamMapMethodArgumentResolver.java | 80 +++ .../RequestParamMethodArgumentResolver.java | 123 +++++ .../support/WebArgumentResolverAdapter.java | 85 ++++ .../HandlerMethodArgumentResolver.java | 59 +++ ...andlerMethodArgumentResolverContainer.java | 114 +++++ .../support/HandlerMethodProcessor.java | 39 ++ .../HandlerMethodReturnValueHandler.java | 54 ++ ...dlerMethodReturnValueHandlerContainer.java | 115 +++++ .../support/InvocableHandlerMethod.java | 241 +++++++++ .../method/support/ModelAndViewContainer.java | 83 +++ .../HandlerSessionAttributeStoreTests.java | 126 +++++ ...nitBinderMethodDataBinderFactoryTests.java | 168 +++++++ .../method/annotation/ModelFactoryTests.java | 199 ++++++++ ...orsMethodHandlerArgumentResolverTests.java | 125 +++++ ...ssionValueMethodArgumentResolverTests.java | 110 ++++ .../ModelAttributeMethodProcessorTests.java | 275 ++++++++++ .../support/ModelMethodProcessorTests.java | 131 +++++ ...tHeaderMapMethodArgumentResolverTests.java | 140 ++++++ ...uestHeaderMethodArgumentResolverTests.java | 151 ++++++ ...stParamMapMethodArgumentResolverTests.java | 118 +++++ ...questParamMethodArgumentResolverTests.java | 161 ++++++ .../WebArgumentResolverAdapterTests.java | 165 ++++++ ...rMethodArgumentResolverContainerTests.java | 103 ++++ ...ethodReturnValueHandlerContainerTests.java | 113 +++++ .../support/InvocableHandlerMethodTests.java | 131 +++++ .../method/support/StubArgumentResolver.java | 67 +++ .../support/StubReturnValueHandler.java | 63 +++ 86 files changed, 11635 insertions(+), 13 deletions(-) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestCondition.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactory.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestKey.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInitBinderMethodDataBinderFactory.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandler.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ModelAndViewMethodReturnValueHandler.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ControllerMethodAnnotationDetectionTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactoryTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyComparatorTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodDetectionTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolverTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMappingTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandlerTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessorTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolverTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolverTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolverTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolverTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/WebDataBinderFactory.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethod.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactory.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/AbstractNamedValueMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/CookieValueMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ErrorsMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelMethodProcessor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapter.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainer.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodProcessor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandler.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainer.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/HandlerSessionAttributeStoreTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactoryTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ErrorsMethodHandlerArgumentResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelMethodProcessorTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapterTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainerTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainerTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/support/StubArgumentResolver.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/method/support/StubReturnValueHandler.java diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java new file mode 100644 index 0000000000..a561a6992d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java @@ -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.servlet.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.ModelAndView; + +/** + * Abstract base class for {@link org.springframework.web.servlet.HandlerExceptionResolver HandlerExceptionResolver} + * implementations that support {@link HandlerMethod HandlerMethod}s. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver { + + /** + * Checks if the handler is a {@link HandlerMethod} instance and performs the check against the bean + * instance it contains. If the provided handler is not an instance of {@link HandlerMethod}, + * {@code false} is returned instead. + */ + @Override + protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { + if (handler == null) { + return super.shouldApplyTo(request, handler); + } + else if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + handler = handlerMethod.getBean(); + return super.shouldApplyTo(request, handler); + } + else { + return false; + } + } + + @Override + protected final ModelAndView doResolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex); + } + + /** + * Actually resolve the given exception that got thrown during on handler execution, + * returning a ModelAndView that represents a specific error page if appropriate. + *

May be overridden in subclasses, in order to apply specific exception checks. + * Note that this template method will be invoked after checking whether this + * resolved applies ("mappedHandlers" etc), so an implementation may simply proceed + * with its actual exception handling. + * @param request current HTTP request + * @param response current HTTP response + * @param handlerMethod the executed handler method, or null if none chosen at the time + * of the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to, or null for default processing + */ + protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, + HttpServletResponse response, + HandlerMethod handlerMethod, + Exception ex) { + return null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java new file mode 100644 index 0000000000..ac0ad59445 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -0,0 +1,284 @@ +/* + * 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.servlet.handler; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.context.ApplicationContextException; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerMethodSelector; + +/** + * Abstract base class for {@link org.springframework.web.servlet.HandlerMapping HandlerMapping} implementations that + * support {@link HandlerMethod}s. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + */ +public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping { + + private final Map handlerMethods = new LinkedHashMap(); + + /** + * Calls the initialization of the superclass and detects handlers. + */ + @Override + public void initApplicationContext() throws ApplicationContextException { + super.initApplicationContext(); + initHandlerMethods(); + } + + /** + * Register handler methods found in beans of the current ApplicationContext. + *

The actual key determination for a handler is up to the concrete + * {@link #getKeyForMethod(Method)} implementation. A method in a bean for which no key + * could be determined is simply not considered a handler method. + * @see #getKeyForMethod(Method) + */ + protected void initHandlerMethods() { + if (logger.isDebugEnabled()) { + logger.debug("Looking for URL mappings in application context: " + getApplicationContext()); + } + for (String beanName : getApplicationContext().getBeanNamesForType(Object.class)) { + if (isHandler(beanName)){ + detectHandlerMethods(beanName); + } + } + } + + /** + * Determines if the given bean is a handler that should be introspected for handler methods. + * @param beanName the name of the bean to check + * @return true if the bean is a handler and may contain handler methods, false otherwise. + */ + protected abstract boolean isHandler(String beanName); + + private void detectHandlerMethods(final String handlerName) { + Class handlerType = getApplicationContext().getType(handlerName); + + Set methods = HandlerMethodSelector.selectMethods(handlerType, new MethodFilter() { + public boolean matches(Method method) { + return getKeyForMethod(method) != null; + } + }); + for (Method method : methods) { + T key = getKeyForMethod(method); + HandlerMethod handlerMethod = new HandlerMethod(handlerName, getApplicationContext(), method); + registerHandlerMethod(key, handlerMethod); + } + } + + /** + * Provides a lookup key for the given method. A method for which no key can be determined is + * not considered a handler method. + * + * @param method the method to create a key for + * @return the lookup key, or {@code null} if the method has none + */ + protected abstract T getKeyForMethod(Method method); + + /** + * Registers a {@link HandlerMethod} under the given key. + * + * @param key the key to register the method under + * @param handlerMethod the handler method to register + * @throws IllegalStateException if another method was already register under the key + */ + protected void registerHandlerMethod(T key, HandlerMethod handlerMethod) { + Assert.notNull(key, "'key' must not be null"); + Assert.notNull(handlerMethod, "'handlerMethod' must not be null"); + HandlerMethod mappedHandlerMethod = handlerMethods.get(key); + if (mappedHandlerMethod != null && !mappedHandlerMethod.equals(handlerMethod)) { + throw new IllegalStateException("Cannot map " + handlerMethod + " to \"" + key + "\": There is already " + + mappedHandlerMethod + " mapped."); + } + handlerMethods.put(key, handlerMethod); + if (logger.isDebugEnabled()) { + logger.debug("Mapped \"" + key + "\" onto " + handlerMethod); + } + } + + @Override + protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + T key = getKeyForRequest(request); + if (key == null) { + return null; + } + if (logger.isDebugEnabled()) { + logger.debug("Looking up handler method for [" + key + "]"); + } + + HandlerMethod handlerMethod = lookupHandlerMethod(key, request); + + if (logger.isDebugEnabled()) { + if (handlerMethod != null) { + logger.debug("Returning [" + handlerMethod + "] as best match for [" + key + "]"); + } + else { + logger.debug("Did not find handler method for [" + key + "]"); + } + } + + return (handlerMethod != null) ? handlerMethod.createWithResolvedBean() : null; + } + + /** + * Abstract template method that returns the lookup key for the given HTTP servlet request. + * + * @param request the request to look up the key for + * @return the key, or {@code null} if the request does not have one + * @throws Exception in case of errors + */ + protected abstract T getKeyForRequest(HttpServletRequest request) throws Exception; + + /** + * Looks up the best-matching {@link HandlerMethod} for the given request. + * + *

This implementation iterators through all handler methods, calls {@link #getMatchingKey(Object, + * HttpServletRequest)} for each of them, {@linkplain #getKeyComparator(HttpServletRequest) sorts} all matches, and + * returns the 1st entry, if any. If no matches are found, {@link #handleNoMatch(Set, HttpServletRequest)} is + * invoked. + * + * @param lookupKey current lookup key + * @param request the current HTTP servlet request + * @return the best-matching handler method, or {@code null} if there is no match + */ + protected HandlerMethod lookupHandlerMethod(T lookupKey, HttpServletRequest request) throws Exception { + if (handlerMethods.containsKey(lookupKey)) { + if (logger.isTraceEnabled()) { + logger.trace("Found direct match for [" + lookupKey + "]"); + } + + handleMatch(lookupKey, request); + return handlerMethods.get(lookupKey); + } + else { + List matches = new ArrayList(); + + for (Map.Entry entry : handlerMethods.entrySet()) { + T match = getMatchingKey(entry.getKey(), request); + if (match != null) { + matches.add(new Match(match, entry.getValue())); + } + } + + if (!matches.isEmpty()) { + Comparator comparator = getMatchComparator(request); + Collections.sort(matches, comparator); + + if (logger.isTraceEnabled()) { + logger.trace("Found " + matches.size() + " matching key(s) for [" + lookupKey + "] : " + matches); + } + + Match bestMatch = matches.get(0); + if (matches.size() > 1) { + Match secondBestMatch = matches.get(1); + if (comparator.compare(bestMatch, secondBestMatch) == 0) { + Method m1 = bestMatch.handlerMethod.getMethod(); + Method m2 = secondBestMatch.handlerMethod.getMethod(); + throw new IllegalStateException( + "Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" + + m1 + ", " + m2 + "}"); + } + } + + handleMatch(bestMatch.key, request); + return bestMatch.handlerMethod; + } + else { + return handleNoMatch(handlerMethods.keySet(), request); + } + } + } + + /** + * Invoked when a key matching to a request has been identified. + * + * @param key the key selected for the request returned by {@link #getMatchingKey(Object, HttpServletRequest)}. + * @param request the current request + */ + protected void handleMatch(T key, HttpServletRequest request) { + } + + /** + * Returns the matching variant of the given key, given the current HTTP servlet request. + * + * @param key the key to get the matches for + * @param request the current HTTP servlet request + * @return the matching key, or {@code null} if the given key does not match against the servlet request + */ + protected abstract T getMatchingKey(T key, HttpServletRequest request); + + private Comparator getMatchComparator(HttpServletRequest request) { + final Comparator keyComparator = getKeyComparator(request); + return new Comparator() { + public int compare(Match m1, Match m2) { + return keyComparator.compare(m1.key, m2.key); + } + }; + } + + /** + * Returns a comparator to sort the keys with. The returned comparator should sort 'better' matches higher. + * + * @param request the current HTTP servlet request + * @return the comparator + */ + protected abstract Comparator getKeyComparator(HttpServletRequest request); + + /** + * Invoked when no match was found. Default implementation returns {@code null}. + * + * @param requestKeys the registered request keys + * @param request the current HTTP request + * @throws ServletException in case of errors + */ + protected HandlerMethod handleNoMatch(Set requestKeys, HttpServletRequest request) throws Exception { + return null; + } + + private class Match { + + private final T key; + + private final HandlerMethod handlerMethod; + + private Match(T key, HandlerMethod handlerMethod) { + this.key = key; + this.handlerMethod = handlerMethod; + } + + @Override + public String toString() { + return key.toString(); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java index f9623c76d4..1ddfbf190b 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -22,7 +22,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -204,9 +204,9 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { } } if (handler != null && this.mappedInterceptors != null) { - Set mappedInterceptors = + HandlerInterceptor[] mappedInterceptors = this.mappedInterceptors.getInterceptors(lookupPath, this.pathMatcher); - if (!mappedInterceptors.isEmpty()) { + if (mappedInterceptors.length != 0) { HandlerExecutionChain chain; if (handler instanceof HandlerExecutionChain) { chain = (HandlerExecutionChain) handler; @@ -214,7 +214,7 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { else { chain = new HandlerExecutionChain(handler); } - chain.addInterceptors(mappedInterceptors.toArray(new HandlerInterceptor[mappedInterceptors.size()])); + chain.addInterceptors(mappedInterceptors); } } if (handler != null && logger.isDebugEnabled()) { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/MappedInterceptors.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/MappedInterceptors.java index b9ae883975..766cd30625 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/MappedInterceptors.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/MappedInterceptors.java @@ -1,29 +1,60 @@ +/* + * 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.servlet.handler; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.HandlerInterceptor; -class MappedInterceptors { - +public final class MappedInterceptors { + private MappedInterceptor[] mappedInterceptors; public MappedInterceptors(MappedInterceptor[] mappedInterceptors) { this.mappedInterceptors = mappedInterceptors; } - - public Set getInterceptors(String lookupPath, PathMatcher pathMatcher) { + + public static MappedInterceptors createFromDeclaredBeans(ListableBeanFactory beanFactory) { + Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, + MappedInterceptor.class, true, false); + + if (!beans.isEmpty()) { + return new MappedInterceptors(beans.values().toArray(new MappedInterceptor[beans.size()])); + } + else { + return null; + } + } + + public HandlerInterceptor[] getInterceptors(String lookupPath, PathMatcher pathMatcher) { Set interceptors = new LinkedHashSet(); for (MappedInterceptor interceptor : this.mappedInterceptors) { if (matches(interceptor, lookupPath, pathMatcher)) { - interceptors.add(interceptor.getInterceptor()); + interceptors.add(interceptor.getInterceptor()); } } - return interceptors; + return interceptors.toArray(new HandlerInterceptor[interceptors.size()]); } - + private boolean matches(MappedInterceptor interceptor, String lookupPath, PathMatcher pathMatcher) { String[] pathPatterns = interceptor.getPathPatterns(); if (pathPatterns != null) { @@ -33,9 +64,10 @@ class MappedInterceptors { } } return false; - } else { + } + else { return true; } } - + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java new file mode 100644 index 0000000000..f4f64d01e9 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java @@ -0,0 +1,112 @@ +/* + * 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.servlet.mvc.method; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.Ordered; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.WebContentGenerator; + +/** + * Abstract base class for {@link HandlerAdapter} implementations that support {@link HandlerMethod}s. + * Contains template methods for handling these handler method. + * + * @author Arjen Poutsma + */ +public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered { + + private int order = Ordered.LOWEST_PRECEDENCE; + + public AbstractHandlerMethodAdapter() { + // no restriction of HTTP methods by default + super(false); + } + + /** + * Specify the order value for this HandlerAdapter bean. + *

Default value is Integer.MAX_VALUE, meaning that it's non-ordered. + * @see org.springframework.core.Ordered#getOrder() + */ + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return this.order; + } + + /** + * {@inheritDoc}

This implementation expects the handler to be an {@link HandlerMethod}. + * + * @param handler the handler instance to check + * @return whether or not this adapter can adapt the given handler + */ + public final boolean supports(Object handler) { + return handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler); + } + + /** + * Given a handler method, return whether or not this adapter can support it. + * + * @param handlerMethod the handler method to check + * @return whether or not this adapter can adapt the given method + */ + protected abstract boolean supportsInternal(HandlerMethod handlerMethod); + + /** + * {@inheritDoc}

This implementation expects the handler to be an {@link HandlerMethod}. + */ + public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + return handleInternal(request, response, (HandlerMethod) handler); + } + + /** + * Use the given handler method to handle the request. + * + * @param request current HTTP request + * @param response current HTTP response + * @param handlerMethod handler method to use. This object must have previously been passed to the + * {@link #supportsInternal(HandlerMethod)} this interface, which must have returned {@code true}. + * @return ModelAndView object with the name of the view and the required model data, or {@code null} if + * the request has been handled directly + * @throws Exception in case of errors + */ + protected abstract ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, + HandlerMethod handlerMethod) throws Exception; + + /** + * {@inheritDoc}

This implementation expects the handler to be an {@link HandlerMethod}. + */ + public final long getLastModified(HttpServletRequest request, Object handler) { + return getLastModifiedInternal(request, (HandlerMethod) handler); + } + + /** + * Same contract as for {@link javax.servlet.http.HttpServlet#getLastModified(HttpServletRequest)}. + * + * @param request current HTTP request + * @param handlerMethod handler method to use + * @return the lastModified value for the given handler + */ + protected abstract long getLastModifiedInternal(HttpServletRequest request, HandlerMethod handlerMethod); +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestCondition.java new file mode 100644 index 0000000000..335066939a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestCondition.java @@ -0,0 +1,29 @@ +/* + * 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.servlet.mvc.method.annotation; + +import javax.servlet.http.HttpServletRequest; + +/** + * A condition that can be matched to a ServletRequest. + * + * @author Rossen Stoyanchev + */ +interface RequestCondition { + + boolean match(HttpServletRequest request); +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactory.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactory.java new file mode 100644 index 0000000000..ef226b3d11 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactory.java @@ -0,0 +1,285 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.MediaType; +import org.springframework.web.util.WebUtils; + +/** + * Factory for request condition objects. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public abstract class RequestConditionFactory { + + /** + * Parses the given parameters, and returns them as a set of request conditions. + * + * @param params the parameters + * @return the request conditions + * @see org.springframework.web.bind.annotation.RequestMapping#params() + */ + public static Set parseParams(String... params) { + if (params == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet(params.length); + for (String expression : params) { + result.add(new ParamNameValueCondition(expression)); + } + return result; + } + + /** + * Parses the given headers, and returns them as a set of request conditions. + * + * @param headers the headers + * @return the request conditions + * @see org.springframework.web.bind.annotation.RequestMapping#headers() + */ + public static Set parseHeaders(String... headers) { + if (headers == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet(headers.length); + for (String expression : headers) { + HeaderNameValueCondition header = new HeaderNameValueCondition(expression); + if (isMediaTypeHeader(header.name)) { + result.add(new MediaTypeHeaderNameValueCondition(expression)); + } + else { + result.add(header); + } + } + return result; + } + + private static boolean isMediaTypeHeader(String name) { + return "Accept".equalsIgnoreCase(name) || "Content-Type".equalsIgnoreCase(name); + } + + /** + * A condition that supports simple "name=value" style expressions as documented in + * @RequestMapping.params() and @RequestMapping.headers(). + */ + private static abstract class AbstractNameValueCondition implements RequestCondition { + + protected final String name; + + protected final T value; + + protected final boolean isNegated; + + protected AbstractNameValueCondition(String expression) { + int separator = expression.indexOf('='); + if (separator == -1) { + this.isNegated = expression.startsWith("!"); + this.name = isNegated ? expression.substring(1) : expression; + this.value = null; + } + else { + this.isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!'); + this.name = isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator); + this.value = parseValue(expression.substring(separator + 1)); + } + } + + protected abstract T parseValue(String valueExpression); + + public final boolean match(HttpServletRequest request) { + boolean isMatch; + if (this.value != null) { + isMatch = matchValue(request); + } + else { + isMatch = matchName(request); + } + return isNegated ? !isMatch : isMatch; + } + + protected abstract boolean matchName(HttpServletRequest request); + + protected abstract boolean matchValue(HttpServletRequest request); + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (value != null) { + builder.append(name); + if (isNegated) { + builder.append('!'); + } + builder.append('='); + builder.append(value); + } + else { + if (isNegated) { + builder.append('!'); + } + builder.append(name); + } + return builder.toString(); + } + + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (isNegated ? 1 : 0); + return result; + } + } + + /** + * Request parameter name-value condition. + */ + private static class ParamNameValueCondition extends AbstractNameValueCondition { + + private ParamNameValueCondition(String expression) { + super(expression); + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(HttpServletRequest request) { + return WebUtils.hasSubmitParameter(request, name); + } + + @Override + protected boolean matchValue(HttpServletRequest request) { + return value.equals(request.getParameter(name)); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof ParamNameValueCondition) { + ParamNameValueCondition other = (ParamNameValueCondition) obj; + return ((this.name.equals(other.name)) && + (this.value != null ? this.value.equals(other.value) : other.value == null) && + this.isNegated == other.isNegated); + } + return false; + } + } + + /** + * Request header name-value condition. + */ + static class HeaderNameValueCondition extends AbstractNameValueCondition { + + public HeaderNameValueCondition(String expression) { + super(expression); + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(HttpServletRequest request) { + return request.getHeader(name) != null; + } + + @Override + final protected boolean matchValue(HttpServletRequest request) { + return value.equals(request.getHeader(name)); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof HeaderNameValueCondition) { + HeaderNameValueCondition other = (HeaderNameValueCondition) obj; + return ((this.name.equalsIgnoreCase(other.name)) && + (this.value != null ? this.value.equals(other.value) : other.value == null) && + this.isNegated == other.isNegated); + } + return false; + } + + + } + + /** + * A RequestCondition that for headers that contain {@link org.springframework.http.MediaType MediaTypes}. + */ + private static class MediaTypeHeaderNameValueCondition extends AbstractNameValueCondition> { + + public MediaTypeHeaderNameValueCondition(String expression) { + super(expression); + } + + @Override + protected List parseValue(String valueExpression) { + return Collections.unmodifiableList(MediaType.parseMediaTypes(valueExpression)); + } + + @Override + protected boolean matchName(HttpServletRequest request) { + return request.getHeader(name) != null; + } + + @Override + protected boolean matchValue(HttpServletRequest request) { + List requestMediaTypes = MediaType.parseMediaTypes(request.getHeader(name)); + + for (MediaType mediaType : this.value) { + for (MediaType requestMediaType : requestMediaTypes) { + if (mediaType.includes(requestMediaType)) { + return true; + } + } + } + return false; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof MediaTypeHeaderNameValueCondition) { + MediaTypeHeaderNameValueCondition other = (MediaTypeHeaderNameValueCondition) obj; + return ((this.name.equalsIgnoreCase(other.name)) && + (this.value != null ? this.value.equals(other.value) : other.value == null) && + this.isNegated == other.isNegated); + } + return false; + } + + + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestKey.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestKey.java new file mode 100644 index 0000000000..988c58ddff --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestKey.java @@ -0,0 +1,326 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.util.UrlPathHelper; + +/** + * TODO + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public final class RequestKey { + + private final Set patterns; + + private final Set methods; + + private final Set paramConditions; + + private final Set headerConditions; + + private int hash; + + /** + * Creates a new {@code RequestKey} instance with the given parameters. + * + *

Package protected for testing purposes. + */ + RequestKey(Collection patterns, + Collection methods, + Collection paramConditions, + Collection headerConditions) { + this.patterns = asUnmodifiableSet(prependLeadingSlash(patterns)); + this.methods = asUnmodifiableSet(methods); + this.paramConditions = asUnmodifiableSet(paramConditions); + this.headerConditions = asUnmodifiableSet(headerConditions); + } + + private static Set prependLeadingSlash(Collection patterns) { + if (patterns == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet(patterns.size()); + for (String pattern : patterns) { + if (!pattern.startsWith("/")) { + pattern = "/" + pattern; + } + result.add(pattern); + } + return result; + } + + private static Set asUnmodifiableSet(Collection collection) { + if (collection == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet(collection); + return Collections.unmodifiableSet(result); + } + + /** + * Creates a new {@code RequestKey} from a {@link RequestMapping @RequestMapping} annotation. + * + * @param annotation the annotation + * @return the request key created from the annotation + */ + public static RequestKey createFromRequestMapping(RequestMapping annotation) { + return new RequestKey(Arrays.asList(annotation.value()), Arrays.asList(annotation.method()), + RequestConditionFactory.parseParams(annotation.params()), + RequestConditionFactory.parseHeaders(annotation.headers())); + } + + /** + * Creates a new {@code RequestKey} from a {@link HttpServletRequest}. + * + * @param request the servlet request + * @param urlPathHelper to create the {@linkplain UrlPathHelper#getLookupPathForRequest(HttpServletRequest) lookup + * path} + * @return the request key created from the servlet request + */ + public static RequestKey createFromServletRequest(HttpServletRequest request, UrlPathHelper urlPathHelper) { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + RequestMethod method = RequestMethod.valueOf(request.getMethod()); + return new RequestKey(Arrays.asList(lookupPath), Arrays.asList(method), null, null); + } + + /** + * Returns the patterns of this request key. + */ + public Set getPatterns() { + return patterns; + } + + /** + * Returns the request methods of this request key. + */ + public Set getMethods() { + return methods; + } + + /** + * Returns the request parameters of this request key. + */ + public Set getParams() { + return paramConditions; + } + + /** + * Returns the request headers of this request key. + */ + public Set getHeaders() { + return headerConditions; + } + + /** + * Creates a new {@code RequestKey} by combining it with another. The typical use case for this is combining type + * and method-level {@link RequestMapping @RequestMapping} annotations. + * + * @param methodKey the method-level RequestKey + * @param pathMatcher to {@linkplain PathMatcher#combine(String, String) combine} the patterns + * @return the combined request key + */ + public RequestKey combine(RequestKey methodKey, PathMatcher pathMatcher) { + Set patterns = combinePatterns(this.patterns, methodKey.patterns, pathMatcher); + Set methods = union(this.methods, methodKey.methods); + Set params = union(this.paramConditions, methodKey.paramConditions); + Set headers = union(this.headerConditions, methodKey.headerConditions); + + return new RequestKey(patterns, methods, params, headers); + } + + private static Set combinePatterns(Collection typePatterns, + Collection methodPatterns, + PathMatcher pathMatcher) { + Set result = new LinkedHashSet(); + if (!typePatterns.isEmpty() && !methodPatterns.isEmpty()) { + for (String pattern1 : typePatterns) { + for (String p2 : methodPatterns) { + result.add(pathMatcher.combine(pattern1, p2)); + } + } + } + else if (!typePatterns.isEmpty()) { + result.addAll(typePatterns); + } + else if (!methodPatterns.isEmpty()) { + result.addAll(methodPatterns); + } + else { + result.add(""); + } + return result; + } + + private static Set union(Collection s1, Collection s2) { + Set union = new LinkedHashSet(s1); + union.addAll(s2); + return union; + } + + /** + * Returns a new {@code RequestKey} that contains all matching attributes of this key, given the {@link + * HttpServletRequest}. Matching patterns in the returned RequestKey are sorted according to {@link + * PathMatcher#getPatternComparator(String)} with the best matching pattern at the top. + * + * @param request the servlet request + * @param pathMatcher to {@linkplain PathMatcher#match(String, String) match} patterns + * @param urlPathHelper to create the {@linkplain UrlPathHelper#getLookupPathForRequest(HttpServletRequest) lookup + * path} + * @return a new request key that contains all matching attributes + */ + public RequestKey getMatchingKey(HttpServletRequest request, PathMatcher pathMatcher, UrlPathHelper urlPathHelper) { + if (!checkMethod(request) || !checkParams(request) || !checkHeaders(request)) { + return null; + } + else { + List matchingPatterns = getMatchingPatterns(request, pathMatcher, urlPathHelper); + if (!matchingPatterns.isEmpty()) { + Set matchingMethods = getMatchingMethods(request); + return new RequestKey(matchingPatterns, matchingMethods, this.paramConditions, this.headerConditions); + } + else { + return null; + } + } + } + + private List getMatchingPatterns(HttpServletRequest request, + PathMatcher pathMatcher, + UrlPathHelper urlPathHelper) { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + + List matchingPatterns = new ArrayList(); + for (String pattern : this.patterns) { + String matchingPattern = getMatchingPattern(pattern, lookupPath, pathMatcher); + if (matchingPattern != null) { + matchingPatterns.add(matchingPattern); + } + } + + Collections.sort(matchingPatterns, pathMatcher.getPatternComparator(lookupPath)); + + return matchingPatterns; + } + + private Set getMatchingMethods(HttpServletRequest request) { + if (this.methods.isEmpty()) { + return this.methods; + } + else { + return Collections.singleton(RequestMethod.valueOf(request.getMethod())); + } + } + + private boolean checkMethod(HttpServletRequest request) { + return methods.isEmpty() || methods.contains(RequestMethod.valueOf(request.getMethod())); + } + + private boolean checkParams(HttpServletRequest request) { + return checkConditions(paramConditions, request); + } + + private boolean checkHeaders(HttpServletRequest request) { + return checkConditions(headerConditions, request); + } + + private String getMatchingPattern(String pattern, String lookupPath, PathMatcher pathMatcher) { + if (pattern.equals(lookupPath) || pathMatcher.match(pattern, lookupPath)) { + return pattern; + } + boolean hasSuffix = pattern.indexOf('.') != -1; + if (!hasSuffix && pathMatcher.match(pattern + ".*", lookupPath)) { + return pattern + ".*"; + } + boolean endsWithSlash = pattern.endsWith("/"); + if (!endsWithSlash && pathMatcher.match(pattern + "/", lookupPath)) { + return pattern +"/"; + } + return null; + } + + private static boolean checkConditions(Set conditions, HttpServletRequest request) { + for (RequestCondition condition : conditions) { + if (!condition.match(request)) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof RequestKey) { + RequestKey other = (RequestKey) obj; + return (this.patterns.equals(other.patterns) && this.methods.equals(other.methods) && + this.paramConditions.equals(other.paramConditions) && + this.headerConditions.equals(other.headerConditions)); + } + return false; + } + + @Override + public int hashCode() { + int result = hash; + if (result == 0) { + result = patterns.hashCode(); + result = 31 * result + methods.hashCode(); + result = 31 * result + paramConditions.hashCode(); + result = 31 * result + headerConditions.hashCode(); + hash = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + builder.append(patterns); + if (!methods.isEmpty()) { + builder.append(','); + builder.append(methods); + } + if (!headerConditions.isEmpty()) { + builder.append(','); + builder.append(headerConditions); + } + if (!paramConditions.isEmpty()) { + builder.append(','); + builder.append(paramConditions); + } + builder.append('}'); + return builder.toString(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java new file mode 100644 index 0000000000..efb199ce81 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -0,0 +1,473 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.xml.transform.Source; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.ui.ModelMap; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.support.DefaultDataBinderFactory; +import org.springframework.web.bind.support.DefaultSessionAttributeStore; +import org.springframework.web.bind.support.SessionAttributeStore; +import org.springframework.web.bind.support.SessionStatus; +import org.springframework.web.bind.support.SimpleSessionStatus; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerMethodSelector; +import org.springframework.web.method.annotation.ModelFactory; +import org.springframework.web.method.annotation.SessionAttributesHandler; +import org.springframework.web.method.annotation.support.ErrorsMethodArgumentResolver; +import org.springframework.web.method.annotation.support.ExpressionValueMethodArgumentResolver; +import org.springframework.web.method.annotation.support.ModelAttributeMethodProcessor; +import org.springframework.web.method.annotation.support.ModelMethodProcessor; +import org.springframework.web.method.annotation.support.RequestHeaderMapMethodArgumentResolver; +import org.springframework.web.method.annotation.support.RequestHeaderMethodArgumentResolver; +import org.springframework.web.method.annotation.support.RequestParamMapMethodArgumentResolver; +import org.springframework.web.method.annotation.support.RequestParamMethodArgumentResolver; +import org.springframework.web.method.annotation.support.WebArgumentResolverAdapter; +import org.springframework.web.method.support.HandlerMethodArgumentResolverContainer; +import org.springframework.web.method.support.HandlerMethodReturnValueHandlerContainer; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; +import org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter; +import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor; +import org.springframework.web.servlet.mvc.method.annotation.support.ModelAndViewMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.support.PathVariableMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletCookieValueMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletModelAttributeMethodProcessor; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; +import org.springframework.web.util.WebUtils; + +/** + * An extension of {@link AbstractHandlerMethodAdapter} with support for {@link RequestMapping} handler methods. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, + InitializingBean { + + private WebArgumentResolver[] customArgumentResolvers; + + private ModelAndViewResolver[] customModelAndViewResolvers; + + private HttpMessageConverter[] messageConverters; + + private WebBindingInitializer webBindingInitializer; + + private int cacheSecondsForSessionAttributeHandlers = 0; + + private boolean synchronizeOnSession = false; + + private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + + private ConfigurableBeanFactory beanFactory; + + private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore(); + + private final Map, SessionAttributesHandler> sessionAttributesHandlerCache = + new ConcurrentHashMap, SessionAttributesHandler>(); + + private final Map, Set> modelAttributeMethodCache = new ConcurrentHashMap, Set>(); + + private final Map, Set> initBinderMethodCache = new ConcurrentHashMap, Set>(); + + private final HandlerMethodReturnValueHandlerContainer returnValueHandlers = new HandlerMethodReturnValueHandlerContainer(); + + private final HandlerMethodArgumentResolverContainer requestMethodArgResolvers = new HandlerMethodArgumentResolverContainer(); + + private final HandlerMethodArgumentResolverContainer binderMethodArgResolvers = new HandlerMethodArgumentResolverContainer(); + + /** + * Create a {@link RequestMappingHandlerAdapter} instance. + */ + public RequestMappingHandlerAdapter() { + + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); + stringHttpMessageConverter.setWriteAcceptCharset(false); // See SPR-7316 + + this.messageConverters = new HttpMessageConverter[] { new ByteArrayHttpMessageConverter(), + stringHttpMessageConverter, new SourceHttpMessageConverter(), + new XmlAwareFormHttpMessageConverter() }; + } + + /** + * Set a custom WebArgumentResolvers to use for special method parameter types. + *

Such a custom WebArgumentResolver will kick in first, having a chance to resolve + * an argument value before the standard argument handling kicks in. + */ + public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { + this.customArgumentResolvers = new WebArgumentResolver[] {argumentResolver}; + } + + /** + * Set one or more custom WebArgumentResolvers to use for special method parameter types. + *

Any such custom WebArgumentResolver will kick in first, having a chance to resolve + * an argument value before the standard argument handling kicks in. + */ + public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { + this.customArgumentResolvers = argumentResolvers; + } + + /** + * Set a custom ModelAndViewResolvers to use for special method return types. + *

Such a custom ModelAndViewResolver will kick in first, having a chance to resolve + * a return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { + this.customModelAndViewResolvers = new ModelAndViewResolver[] {customModelAndViewResolver}; + } + + /** + * Set one or more custom ModelAndViewResolvers to use for special method return types. + *

Any such custom ModelAndViewResolver will kick in first, having a chance to resolve + * a return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) { + this.customModelAndViewResolvers = customModelAndViewResolvers; + } + + /** + * Set the message body converters to use. + *

These converters are used to convert from and to HTTP requests and responses. + */ + public void setMessageConverters(HttpMessageConverter[] messageConverters) { + this.messageConverters = messageConverters; + } + + /** + * Specify a WebBindingInitializer which will apply pre-configured + * configuration to every DataBinder that this controller uses. + */ + public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { + this.webBindingInitializer = webBindingInitializer; + } + + /** + * Specify the strategy to store session attributes with. + *

Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore}, + * storing session attributes in the HttpSession, using the same attribute name as in the model. + */ + public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { + this.sessionAttributeStore = sessionAttributeStore; + } + + /** + * Cache content produced by @SessionAttributes annotated handlers + * for the given number of seconds. Default is 0, preventing caching completely. + *

In contrast to the "cacheSeconds" property which will apply to all general handlers + * (but not to @SessionAttributes annotated handlers), this setting will + * apply to @SessionAttributes annotated handlers only. + * @see #setCacheSeconds + * @see org.springframework.web.bind.annotation.SessionAttributes + */ + public void setCacheSecondsForSessionAttributeHandlers(int cacheSecondsForSessionAttributeHandlers) { + this.cacheSecondsForSessionAttributeHandlers = cacheSecondsForSessionAttributeHandlers; + } + + /** + * Set if controller execution should be synchronized on the session, + * to serialize parallel invocations from the same client. + *

More specifically, the execution of the handleRequestInternal + * method will get synchronized if this flag is "true". The best available + * session mutex will be used for the synchronization; ideally, this will + * be a mutex exposed by HttpSessionMutexListener. + *

The session mutex is guaranteed to be the same object during + * the entire lifetime of the session, available under the key defined + * by the SESSION_MUTEX_ATTRIBUTE constant. It serves as a + * safe reference to synchronize on for locking on the current session. + *

In many cases, the HttpSession reference itself is a safe mutex + * as well, since it will always be the same object reference for the + * same active logical session. However, this is not guaranteed across + * different servlet containers; the only 100% safe way is a session mutex. + * @see org.springframework.web.util.HttpSessionMutexListener + * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession) + */ + public void setSynchronizeOnSession(boolean synchronizeOnSession) { + this.synchronizeOnSession = synchronizeOnSession; + } + + /** + * Set the ParameterNameDiscoverer to use for resolving method parameter names if needed + * (e.g. for default attribute names). + *

Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableBeanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + } + + public void afterPropertiesSet() throws Exception { + initRequestMethodArgResolvers(); + initBinderMethodArgResolvers(); + initReturnValueHandlers(); + } + + private void initRequestMethodArgResolvers() { + requestMethodArgResolvers.registerArgumentResolver(new RequestParamMethodArgumentResolver(beanFactory, false)); + requestMethodArgResolvers.registerArgumentResolver(new RequestParamMapMethodArgumentResolver()); + requestMethodArgResolvers.registerArgumentResolver(new PathVariableMethodArgumentResolver(beanFactory)); + requestMethodArgResolvers.registerArgumentResolver(new ServletModelAttributeMethodProcessor(false)); + requestMethodArgResolvers.registerArgumentResolver(new RequestResponseBodyMethodProcessor(messageConverters)); + requestMethodArgResolvers.registerArgumentResolver(new RequestHeaderMethodArgumentResolver(beanFactory)); + requestMethodArgResolvers.registerArgumentResolver(new RequestHeaderMapMethodArgumentResolver()); + requestMethodArgResolvers.registerArgumentResolver(new ServletCookieValueMethodArgumentResolver(beanFactory)); + requestMethodArgResolvers.registerArgumentResolver(new ExpressionValueMethodArgumentResolver(beanFactory)); + + if (customArgumentResolvers != null) { + for (WebArgumentResolver customResolver : customArgumentResolvers) { + requestMethodArgResolvers.registerArgumentResolver(new WebArgumentResolverAdapter(customResolver)); + } + } + + requestMethodArgResolvers.registerArgumentResolver(new ServletRequestMethodArgumentResolver()); + requestMethodArgResolvers.registerArgumentResolver(new ServletResponseMethodArgumentResolver()); + requestMethodArgResolvers.registerArgumentResolver(new HttpEntityMethodProcessor(messageConverters)); + requestMethodArgResolvers.registerArgumentResolver(new ModelMethodProcessor()); + requestMethodArgResolvers.registerArgumentResolver(new ErrorsMethodArgumentResolver()); + requestMethodArgResolvers.registerArgumentResolver(new RequestParamMethodArgumentResolver(beanFactory, true)); + requestMethodArgResolvers.registerArgumentResolver(new ServletModelAttributeMethodProcessor(true)); + } + + private void initBinderMethodArgResolvers() { + binderMethodArgResolvers.registerArgumentResolver(new RequestParamMethodArgumentResolver(beanFactory, false)); + binderMethodArgResolvers.registerArgumentResolver(new RequestParamMapMethodArgumentResolver()); + binderMethodArgResolvers.registerArgumentResolver(new PathVariableMethodArgumentResolver(beanFactory)); + binderMethodArgResolvers.registerArgumentResolver(new ExpressionValueMethodArgumentResolver(beanFactory)); + + if (customArgumentResolvers != null) { + for (WebArgumentResolver customResolver : customArgumentResolvers) { + binderMethodArgResolvers.registerArgumentResolver(new WebArgumentResolverAdapter(customResolver)); + } + } + + binderMethodArgResolvers.registerArgumentResolver(new ServletRequestMethodArgumentResolver()); + binderMethodArgResolvers.registerArgumentResolver(new ServletResponseMethodArgumentResolver()); + binderMethodArgResolvers.registerArgumentResolver(new RequestParamMethodArgumentResolver(beanFactory, true)); + } + + private void initReturnValueHandlers() { + returnValueHandlers.registerReturnValueHandler(new RequestResponseBodyMethodProcessor(messageConverters)); + returnValueHandlers.registerReturnValueHandler(new ModelAttributeMethodProcessor(false)); + returnValueHandlers.registerReturnValueHandler(new ModelAndViewMethodReturnValueHandler()); + returnValueHandlers.registerReturnValueHandler(new ModelMethodProcessor()); + returnValueHandlers.registerReturnValueHandler(new ViewMethodReturnValueHandler()); + returnValueHandlers.registerReturnValueHandler(new HttpEntityMethodProcessor(messageConverters)); + returnValueHandlers.registerReturnValueHandler(new DefaultMethodReturnValueHandler(customModelAndViewResolvers)); + } + + @Override + protected boolean supportsInternal(HandlerMethod handlerMethod) { + return supportsMethodParameters(handlerMethod.getMethodParameters()) && + supportsReturnType(handlerMethod.getReturnType()); + } + + private boolean supportsMethodParameters(MethodParameter[] methodParameters) { + for (MethodParameter methodParameter : methodParameters) { + if (! this.requestMethodArgResolvers.supportsParameter(methodParameter)) { + return false; + } + } + return true; + } + + private boolean supportsReturnType(MethodParameter methodReturnType) { + return (this.returnValueHandlers.supportsReturnType(methodReturnType) || + Void.TYPE.equals(methodReturnType.getParameterType())); + } + + @Override + protected long getLastModifiedInternal(HttpServletRequest request, HandlerMethod handlerMethod) { + return -1; + } + + @Override + protected final ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, + HandlerMethod handlerMethod) throws Exception { + + if (hasSessionAttributes(handlerMethod.getBeanType())) { + // Always prevent caching in case of session attribute management. + checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); + } + else { + // Uses configured default cacheSeconds setting. + checkAndPrepare(request, response, true); + } + + // Execute invokeHandlerMethod in synchronized block if required. + if (this.synchronizeOnSession) { + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + synchronized (mutex) { + return invokeHandlerMethod(request, response, handlerMethod); + } + } + } + + return invokeHandlerMethod(request, response, handlerMethod); + } + + private boolean hasSessionAttributes(Class handlerType) { + SessionAttributesHandler handler = null; + synchronized(this.sessionAttributesHandlerCache) { + handler = this.sessionAttributesHandlerCache.get(handlerType); + if (handler == null) { + handler = new SessionAttributesHandler(handlerType, sessionAttributeStore); + this.sessionAttributesHandlerCache.put(handlerType, handler); + } + } + return handler.hasSessionAttributes(); + } + + private ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, + HandlerMethod handlerMethod) throws Exception { + + WebDataBinderFactory binderFactory = createDataBinderFactory(handlerMethod); + ModelFactory modelFactory = createModelFactory(handlerMethod, binderFactory); + ServletInvocableHandlerMethod requestMethod = createRequestMappingMethod(handlerMethod, binderFactory); + + ServletWebRequest webRequest = new ServletWebRequest(request, response); + SessionStatus sessionStatus = new SimpleSessionStatus(); + + ModelMap implicitModel = modelFactory.createModel(webRequest, requestMethod); + ModelAndView mav = requestMethod.invokeAndHandle(webRequest, implicitModel, sessionStatus); + + ModelMap actualModel = (mav != null) ? mav.getModelMap() : null; + modelFactory.updateAttributes(webRequest, sessionStatus, actualModel, implicitModel); + + return mav; + } + + private WebDataBinderFactory createDataBinderFactory(HandlerMethod handlerMethod) { + List initBinderMethods = new ArrayList(); + + Class handlerType = handlerMethod.getBeanType(); + Set binderMethods = initBinderMethodCache.get(handlerType); + if (binderMethods == null) { + binderMethods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS); + initBinderMethodCache.put(handlerType, binderMethods); + } + + for (Method method : binderMethods) { + Object bean = handlerMethod.getBean(); + InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method); + binderMethod.setArgumentResolverContainer(this.binderMethodArgResolvers); + binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer)); + binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + + initBinderMethods.add(binderMethod); + } + + return new ServletInitBinderMethodDataBinderFactory(initBinderMethods, this.webBindingInitializer); + } + + private ModelFactory createModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) { + List modelAttrMethods = new ArrayList(); + + Class handlerType = handlerMethod.getBeanType(); + Set attributeMethods = modelAttributeMethodCache.get(handlerType); + if (attributeMethods == null) { + attributeMethods = HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS); + modelAttributeMethodCache.put(handlerType, attributeMethods); + } + + for (Method method : attributeMethods) { + InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method); + attrMethod.setArgumentResolverContainer(this.requestMethodArgResolvers); + attrMethod.setDataBinderFactory(binderFactory); + attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + modelAttrMethods.add(attrMethod); + } + + return new ModelFactory(modelAttrMethods, binderFactory, sessionAttributesHandlerCache.get(handlerType)); + } + + private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod, + WebDataBinderFactory binderFactory) { + Method method = handlerMethod.getMethod(); + ServletInvocableHandlerMethod requestMethod = new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); + requestMethod.setArgumentResolverContainer(this.requestMethodArgResolvers); + requestMethod.setReturnValueHandlers(this.returnValueHandlers); + requestMethod.setDataBinderFactory(binderFactory); + requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + return requestMethod; + } + + /** + * MethodFilter that matches {@link InitBinder @InitBinder} methods. + */ + public static MethodFilter INIT_BINDER_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return AnnotationUtils.findAnnotation(method, InitBinder.class) != null; + } + }; + + /** + * MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods. + */ + public static MethodFilter MODEL_ATTRIBUTE_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return ((AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) && + (AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null)); + } + }; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolver.java new file mode 100644 index 0000000000..d77eb37a54 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolver.java @@ -0,0 +1,222 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.transform.Source; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerMethodSelector; +import org.springframework.web.method.annotation.ExceptionMethodMapping; +import org.springframework.web.method.annotation.support.ModelAttributeMethodProcessor; +import org.springframework.web.method.annotation.support.ModelMethodProcessor; +import org.springframework.web.method.annotation.support.WebArgumentResolverAdapter; +import org.springframework.web.method.support.HandlerMethodArgumentResolverContainer; +import org.springframework.web.method.support.HandlerMethodReturnValueHandlerContainer; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver; +import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor; +import org.springframework.web.servlet.mvc.method.annotation.support.ModelAndViewMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; + +/** + * An extension of {@link AbstractHandlerMethodExceptionResolver} that matches thrown exceptions to + * {@link ExceptionHandler @ExceptionHandler} methods in the handler. If a match is found the + * exception-handling method is invoked to process the request. + * + *

See {@link ExceptionHandler} for information on supported method arguments and return values + * for exception-handling methods. You can customize method argument resolution and return value + * processing through the various bean properties in this class. + * + * @author Rossen Stoyanchev + * @since 3.1 + * @see #setCustomArgumentResolvers(WebArgumentResolver[]) + * @see #setCustomModelAndViewResolvers(ModelAndViewResolver[]) + * @see #setMessageConverters(HttpMessageConverter[]) + */ +public class RequestMappingHandlerMethodExceptionResolver extends AbstractHandlerMethodExceptionResolver implements + InitializingBean { + + private WebArgumentResolver[] customArgumentResolvers; + + private HttpMessageConverter[] messageConverters; + + private ModelAndViewResolver[] customModelAndViewResolvers; + + private final Map, ExceptionMethodMapping> exceptionMethodMappingCache = + new ConcurrentHashMap, ExceptionMethodMapping>(); + + private final HandlerMethodArgumentResolverContainer argumentResolvers = new HandlerMethodArgumentResolverContainer(); + + private final HandlerMethodReturnValueHandlerContainer returnValueHandlers = new HandlerMethodReturnValueHandlerContainer(); + + /** + * Creates an instance of {@link RequestMappingHandlerMethodExceptionResolver}. + */ + public RequestMappingHandlerMethodExceptionResolver() { + + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); + stringHttpMessageConverter.setWriteAcceptCharset(false); // See SPR-7316 + + this.messageConverters = new HttpMessageConverter[] { new ByteArrayHttpMessageConverter(), + stringHttpMessageConverter, new SourceHttpMessageConverter(), + new XmlAwareFormHttpMessageConverter() }; + } + + /** + * Set a custom ArgumentResolvers to use for special method parameter types. + *

Such a custom ArgumentResolver will kick in first, having a chance to resolve + * an argument value before the standard argument handling kicks in. + */ + public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { + this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver}; + } + + /** + * Set one or more custom ArgumentResolvers to use for special method parameter types. + *

Any such custom ArgumentResolver will kick in first, having a chance to resolve + * an argument value before the standard argument handling kicks in. + */ + public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { + this.customArgumentResolvers = argumentResolvers; + } + + /** + * Set the message body converters to use. + *

These converters are used to convert from and to HTTP requests and responses. + */ + public void setMessageConverters(HttpMessageConverter[] messageConverters) { + this.messageConverters = messageConverters; + } + + /** + * Set a custom ModelAndViewResolvers to use for special method return types. + *

Such a custom ModelAndViewResolver will kick in first, having a chance to resolve + * a return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { + this.customModelAndViewResolvers = new ModelAndViewResolver[] {customModelAndViewResolver}; + } + + /** + * Set one or more custom ModelAndViewResolvers to use for special method return types. + *

Any such custom ModelAndViewResolver will kick in first, having a chance to resolve + * a return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) { + this.customModelAndViewResolvers = customModelAndViewResolvers; + } + + public void afterPropertiesSet() throws Exception { + if (customArgumentResolvers != null) { + for (WebArgumentResolver customResolver : customArgumentResolvers) { + argumentResolvers.registerArgumentResolver(new WebArgumentResolverAdapter(customResolver)); + } + } + + argumentResolvers.registerArgumentResolver(new ServletRequestMethodArgumentResolver()); + argumentResolvers.registerArgumentResolver(new ServletResponseMethodArgumentResolver()); + + returnValueHandlers.registerReturnValueHandler(new RequestResponseBodyMethodProcessor(messageConverters)); + returnValueHandlers.registerReturnValueHandler(new ModelAttributeMethodProcessor(false)); + returnValueHandlers.registerReturnValueHandler(new ModelAndViewMethodReturnValueHandler()); + returnValueHandlers.registerReturnValueHandler(new ModelMethodProcessor()); + returnValueHandlers.registerReturnValueHandler(new ViewMethodReturnValueHandler()); + returnValueHandlers.registerReturnValueHandler(new HttpEntityMethodProcessor(messageConverters)); + returnValueHandlers.registerReturnValueHandler(new DefaultMethodReturnValueHandler(customModelAndViewResolvers)); + } + + @Override + protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, + HttpServletResponse response, + HandlerMethod handlerMethod, + Exception ex) { + if (handlerMethod != null) { + ExceptionMethodMapping mapping = getExceptionMethodMapping(handlerMethod); + Method method = mapping.getMethod(ex); + + if (method != null) { + Object handler = handlerMethod.getBean(); + ServletInvocableHandlerMethod exceptionHandler = new ServletInvocableHandlerMethod(handler, method); + exceptionHandler.setArgumentResolverContainer(argumentResolvers); + exceptionHandler.setReturnValueHandlers(returnValueHandlers); + + ServletWebRequest webRequest = new ServletWebRequest(request, response); + ModelMap model = new ExtendedModelMap(); + try { + if (logger.isDebugEnabled()) { + logger.debug("Invoking exception-handling method: " + exceptionHandler); + } + ModelAndView mav = exceptionHandler.invokeAndHandle(webRequest , model , ex); + return (mav != null) ? mav : new ModelAndView(); + } + catch (Exception invocationEx) { + logger.error("Invoking exception-handling method resulted in exception : " + + exceptionHandler, invocationEx); + } + } + } + + return null; + } + + private ExceptionMethodMapping getExceptionMethodMapping(HandlerMethod handlerMethod) { + Class handlerType = handlerMethod.getBeanType(); + ExceptionMethodMapping mapping = exceptionMethodMappingCache.get(handlerType); + if (mapping == null) { + Set methods = HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS); + mapping = new ExceptionMethodMapping(methods); + exceptionMethodMappingCache.put(handlerType, mapping); + } + return mapping; + } + + /** + * Pre-built MethodFilter that matches {@link ExceptionHandler @ExceptionHandler} methods. + */ + public static MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return AnnotationUtils.findAnnotation(method, ExceptionHandler.class) != null; + } + }; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMapping.java new file mode 100644 index 0000000000..8e6a5d89e4 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMapping.java @@ -0,0 +1,320 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.lang.reflect.Method; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; +import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.servlet.handler.MappedInterceptors; +import org.springframework.web.util.UrlPathHelper; + +/** + * TODO + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1.0 + */ +public class RequestMappingHandlerMethodMapping extends AbstractHandlerMethodMapping { + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + private MappedInterceptors mappedInterceptors; + + /** + * Set if URL lookup should always use the full path within the current servlet context. Else, the path within the + * current servlet mapping is used if applicable (that is, in the case of a ".../*" servlet mapping in web.xml). + *

Default is "false". + * + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. Both are returned undecoded by the Servlet API, in + * contrast to the servlet path.

Uses either the request encoding or the default encoding according to the Servlet + * spec (ISO-8859-1). + * + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths.

Use this to override the default UrlPathHelper + * with a custom subclass, or to share common UrlPathHelper settings across multiple HandlerMappings and + * MethodNameResolvers. + * + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths against registered URL patterns. Default is + * AntPathMatcher. + * + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + /** + * Set the {@link MappedInterceptor} instances to use to intercept handler method invocations. + */ + public void setMappedInterceptors(MappedInterceptor[] mappedInterceptors) { + this.mappedInterceptors = new MappedInterceptors(mappedInterceptors); + } + + @Override + protected void initInterceptors() { + super.initInterceptors(); + if (this.mappedInterceptors == null) { + this.mappedInterceptors = MappedInterceptors.createFromDeclaredBeans(getApplicationContext()); + } + } + + /** + * {@inheritDoc} + * The handler determination is made based on the presence of a type-level {@link Controller} or + * a type-level {@link RequestMapping} annotation. + */ + @Override + protected boolean isHandler(String beanName) { + return ((getApplicationContext().findAnnotationOnBean(beanName, RequestMapping.class) != null) || + (getApplicationContext().findAnnotationOnBean(beanName, Controller.class) != null)); + } + + /** + * Returns a {@link RequestKey} instances that represents the given HTTP servlet request. + * + * @param request the request to look up the key for + * @return the key, never null + */ + @Override + protected RequestKey getKeyForRequest(HttpServletRequest request) { + return RequestKey.createFromServletRequest(request, urlPathHelper); + } + + /** + * Provides a {@link RequestKey} for the given method. + *

Only {@link RequestMapping @RequestMapping}-annotated methods are considered. + * Type-level {@link RequestMapping @RequestMapping} annotations are also detected and their + * attributes combined with method-level {@link RequestMapping @RequestMapping} attributes. + * + * @param method the method to create a key for + * @return the key, or {@code null} + * @see RequestKey#combine(RequestKey, PathMatcher) + */ + @Override + protected RequestKey getKeyForMethod(Method method) { + RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (annotation != null) { + RequestKey methodKey = RequestKey.createFromRequestMapping(annotation); + RequestMapping typeAnnot = AnnotationUtils.findAnnotation(method.getDeclaringClass(), RequestMapping.class); + if (typeAnnot != null) { + RequestKey typeKey = RequestKey.createFromRequestMapping(typeAnnot); + return typeKey.combine(methodKey, pathMatcher); + } + else { + return methodKey; + } + } + else { + return null; + } + } + + @Override + protected Comparator getKeyComparator(HttpServletRequest request) { + return new RequestKeyComparator(request); + } + + @Override + protected void handleMatch(RequestKey key, HttpServletRequest request) { + String pattern = key.getPatterns().iterator().next(); + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + Map uriTemplateVariables = pathMatcher.extractUriTemplateVariables(pattern, lookupPath); + request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables); + } + + @Override + protected RequestKey getMatchingKey(RequestKey key, HttpServletRequest request) { + return key.getMatchingKey(request, pathMatcher, urlPathHelper); + } + + @Override + protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { + HandlerExecutionChain chain = super.getHandlerExecutionChain(handler, request); + if (this.mappedInterceptors != null) { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + HandlerInterceptor[] handlerInterceptors = mappedInterceptors.getInterceptors(lookupPath, pathMatcher); + if (handlerInterceptors.length > 0) { + chain.addInterceptors(handlerInterceptors); + } + } + return chain; + } + + @Override + protected HandlerMethod handleNoMatch(Set requestKeys, HttpServletRequest request) + throws HttpRequestMethodNotSupportedException { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + Set allowedMethods = new HashSet(6); + for (RequestKey requestKey : requestKeys) { + for (String pattern : requestKey.getPatterns()) { + if (pathMatcher.match(pattern, lookupPath)) { + for (RequestMethod method : requestKey.getMethods()) { + allowedMethods.add(method.name()); + } + } + } + } + if (!allowedMethods.isEmpty()) { + throw new HttpRequestMethodNotSupportedException(request.getMethod(), + allowedMethods.toArray(new String[allowedMethods.size()])); + + } else { + return null; + } + } + + /** + * A comparator for RequestKey types. Effective comparison can only be done in the context of a specific request. For + * example not all configured patterns may apply to the current request. Therefore an HttpServletRequest is required as + * input. + * + * Furthermore, the following assumptions are made about the input RequestKeys:

+ * + * @see RequestMappingHandlerMethodMapping#getMatchingKey(RequestKey, HttpServletRequest) + */ + private class RequestKeyComparator implements Comparator { + + private Comparator patternComparator; + + private List requestAcceptHeader; + + public RequestKeyComparator(HttpServletRequest request) { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + this.patternComparator = pathMatcher.getPatternComparator(lookupPath); + String acceptHeader = request.getHeader("Accept"); + this.requestAcceptHeader = MediaType.parseMediaTypes(acceptHeader); + MediaType.sortByQualityValue(this.requestAcceptHeader); + } + + public int compare(RequestKey key, RequestKey otherKey) { + int result = comparePatterns(key.getPatterns(), otherKey.getPatterns()); + if (result != 0) { + return result; + } + result = otherKey.getParams().size() - key.getParams().size(); + if (result != 0) { + return result; + } + result = otherKey.getHeaders().size() - key.getHeaders().size(); + if (result != 0) { + return result; + } +/* + TODO: fix + result = compareAcceptHeaders(key.getAcceptHeaderMediaTypes(), otherKey.getAcceptHeaderMediaTypes()); + if (result != 0) { + return result; + } +*/ + result = otherKey.getMethods().size() - key.getMethods().size(); + if (result != 0) { + return result; + } + return 0; + } + + private int comparePatterns(Set patterns, Set otherPatterns) { + Iterator iterator = patterns.iterator(); + Iterator iteratorOther = otherPatterns.iterator(); + while (iterator.hasNext() && iteratorOther.hasNext()) { + int result = patternComparator.compare(iterator.next(), iteratorOther.next()); + if (result != 0) { + return result; + } + } + if (iterator.hasNext()) { + return -1; + } + else if (iteratorOther.hasNext()) { + return 1; + } + else { + return 0; + } + } + + private int compareAcceptHeaders(List accept, List otherAccept) { + for (MediaType requestAccept : this.requestAcceptHeader) { + int pos1 = indexOfIncluded(requestAccept, accept); + int pos2 = indexOfIncluded(requestAccept, otherAccept); + if (pos1 != pos2) { + return pos2 - pos1; + } + } + return 0; + } + + private int indexOfIncluded(MediaType requestAccept, List accept) { + for (int i = 0; i < accept.size(); i++) { + if (requestAccept.includes(accept.get(i))) { + return i; + } + } + return -1; + } + + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInitBinderMethodDataBinderFactory.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInitBinderMethodDataBinderFactory.java new file mode 100644 index 0000000000..ef1728f619 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInitBinderMethodDataBinderFactory.java @@ -0,0 +1,53 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.util.List; + +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.method.annotation.InitBinderMethodDataBinderFactory; +import org.springframework.web.method.support.InvocableHandlerMethod; + +/** + * An {@link InitBinderMethodDataBinderFactory} for Servlet environments. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ServletInitBinderMethodDataBinderFactory extends InitBinderMethodDataBinderFactory { + + /** + * Create an {@link ServletInitBinderMethodDataBinderFactory} instance. + * @param initBinderMethods init binder methods to use to initialize new data binders. + * @param bindingInitializer a WebBindingInitializer to use to initialize created data binder instances. + */ + public ServletInitBinderMethodDataBinderFactory(List initBinderMethods, + WebBindingInitializer bindingInitializer) { + super(initBinderMethods, bindingInitializer); + } + + /** + * {@inheritDoc} creates a Servlet data binder. + */ + @Override + protected WebDataBinder createBinderInstance(Object target, String objectName) { + return new ServletRequestDataBinder(target, objectName); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java new file mode 100644 index 0000000000..522cdec899 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -0,0 +1,160 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.lang.reflect.Method; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.support.SessionStatus; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodProcessor; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.method.support.HandlerMethodReturnValueHandlerContainer; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; + +/** + * Extends {@link InvocableHandlerMethod} with the ability to handle the return value of the invocation + * resulting in a {@link ModelAndView} according to the {@link HandlerAdapter} contract. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { + + private HttpStatus responseStatus; + + private String responseReason; + + private HandlerMethodReturnValueHandlerContainer returnValueHandlers; + + public void setReturnValueHandlers(HandlerMethodReturnValueHandlerContainer returnValueHandlers) { + this.returnValueHandlers = returnValueHandlers; + } + + /** + * Creates a {@link ServletInvocableHandlerMethod} instance with the given bean and method. + * @param handler the object handler + * @param method the method + */ + public ServletInvocableHandlerMethod(Object handler, Method method) { + super(handler, method); + + ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class); + if (annotation != null) { + this.responseStatus = annotation.value(); + this.responseReason = annotation.reason(); + + } + } + + /** + * Invokes the method via {@link #invokeForRequest(NativeWebRequest, ModelMap, Object...)} and also handles the + * return value by invoking one of the {@link HandlerMethodReturnValueHandler} instances registered via + * {@link #setReturnValueHandlers(HandlerMethodReturnValueHandlerContainer)}. + * If the method is annotated with {@link SessionStatus} the response status will be set. + * @param request the current request + * @param model the model used throughout the current request + * @param providedArgs argument values to use as-is if they match to a method parameter's type + * @return ModelAndView object with the name of the view and the required model data, or null + * if the response was handled + */ + public final ModelAndView invokeAndHandle(NativeWebRequest request, + ModelMap model, + Object... providedArgs) throws Exception { + + if (!returnValueHandlers.supportsReturnType(getReturnType())) { + throw new IllegalStateException("No suitable HandlerMethodReturnValueHandler for method " + toString()); + } + + Object returnValue = invokeForRequest(request, model, providedArgs); + + setResponseStatus((ServletWebRequest) request); + + ModelAndViewContainer mavContainer = new ModelAndViewContainer(model); + returnValueHandlers.handleReturnValue(returnValue, getReturnType(), mavContainer, request); + + return getModelAndView(request, mavContainer, returnValue); + } + + /** + * Set the response status according to the {@link ResponseStatus} annotation. + */ + private void setResponseStatus(ServletWebRequest webRequest) throws IOException { + if (this.responseStatus != null) { + if (StringUtils.hasText(this.responseReason)) { + webRequest.getResponse().sendError(this.responseStatus.value(), this.responseReason); + } + else { + webRequest.getResponse().sendError(this.responseStatus.value()); + } + + // to be picked up by the RedirectView + webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, this.responseStatus); + } + } + + /** + * Create a {@link ModelAndView} from a {@link ModelAndViewContainer}. + */ + private ModelAndView getModelAndView(NativeWebRequest request, + ModelAndViewContainer mavContainer, + Object returnValue) { + if (returnValue == null && isResponseHandled(request)) { + return null; + } + else if (returnValueHandlerUsesResponseArgument()) { + return null; + } + else { + ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel()); + mav.setViewName(mavContainer.getViewName()); + if (mavContainer.getView() != null) { + mav.setView(mavContainer.getView()); + } + return mav; + } + } + + private boolean isResponseHandled(NativeWebRequest request) { + ServletWebRequest servletRequest = (ServletWebRequest) request; + return (servletRequest.isNotModified() || (responseStatus != null) || usesResponseArgument()); + } + + /** + * Whether any of the underlying {@link HandlerMethodArgumentResolver}s or + * {@link HandlerMethodReturnValueHandler}s use the response argument. + * @see HandlerMethodProcessor#usesResponseArgument(MethodParameter) + */ + protected boolean usesResponseArgument() { + return (super.usesResponseArgument() || returnValueHandlerUsesResponseArgument()); + } + + private boolean returnValueHandlerUsesResponseArgument() { + return returnValueHandlers.usesResponseArgument(getReturnType()); + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java new file mode 100644 index 0000000000..62754a241a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java @@ -0,0 +1,146 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; + +/** + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + */ +public abstract class AbstractMessageConverterMethodProcessor + implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final HttpMessageConverter[] messageConverters; + + protected AbstractMessageConverterMethodProcessor(HttpMessageConverter... messageConverters) { + Assert.notNull(messageConverters, "'messageConverters' must not be null"); + this.messageConverters = messageConverters; + } + + @SuppressWarnings("unchecked") + protected Object readWithMessageConverters(NativeWebRequest webRequest, + MethodParameter methodParam, + Class paramType) + throws IOException, HttpMediaTypeNotSupportedException { + + HttpInputMessage inputMessage = createInputMessage(webRequest); + + MediaType contentType = inputMessage.getHeaders().getContentType(); + if (contentType == null) { + StringBuilder builder = new StringBuilder(ClassUtils.getShortName(methodParam.getParameterType())); + String paramName = methodParam.getParameterName(); + if (paramName != null) { + builder.append(' '); + builder.append(paramName); + } + throw new HttpMediaTypeNotSupportedException("Cannot read parameter (" + builder.toString() + + ") using HttpMessageConverters: no Content-Type found in HTTP request"); + } + + List allSupportedMediaTypes = new ArrayList(); + if (this.messageConverters != null) { + for (HttpMessageConverter messageConverter : this.messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + if (messageConverter.canRead(paramType, contentType)) { + if (logger.isDebugEnabled()) { + logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" + + messageConverter + "]"); + } + return ((HttpMessageConverter) messageConverter).read(paramType, inputMessage); + } + } + } + + throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes); + } + + protected abstract HttpInputMessage createInputMessage(NativeWebRequest webRequest); + + protected void writeWithMessageConverters(NativeWebRequest webRequest, Object returnValue) + throws IOException, HttpMediaTypeNotAcceptableException { + writeWithMessageConverters(returnValue, createInputMessage(webRequest), createOutputMessage(webRequest)); + } + + protected abstract HttpOutputMessage createOutputMessage(NativeWebRequest webRequest); + + @SuppressWarnings("unchecked") + protected void writeWithMessageConverters(T returnValue, + HttpInputMessage inputMessage, + HttpOutputMessage outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException { + + List acceptedMediaTypes = getAcceptedMediaTypes(inputMessage); + + List allSupportedMediaTypes = new ArrayList(); + if (this.messageConverters != null) { + for (MediaType acceptedMediaType : acceptedMediaTypes) { + for (HttpMessageConverter messageConverter : this.messageConverters) { + if (!messageConverter.canWrite(returnValue.getClass(), acceptedMediaType)) { + continue; + } + ((HttpMessageConverter) messageConverter).write(returnValue, acceptedMediaType, outputMessage); + if (logger.isDebugEnabled()) { + MediaType contentType = outputMessage.getHeaders().getContentType(); + if (contentType == null) { + contentType = acceptedMediaType; + } + logger.debug("Written [" + returnValue + "] as \"" + contentType + "\" using [" + + messageConverter + "]"); + } + return; + } + } + for (HttpMessageConverter messageConverter : messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + } + } + throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes); + } + + private List getAcceptedMediaTypes(HttpInputMessage inputMessage) { + List acceptedMediaTypes = inputMessage.getHeaders().getAccept(); + if (acceptedMediaTypes.isEmpty()) { + acceptedMediaTypes = Collections.singletonList(MediaType.ALL); + } + + MediaType.sortByQualityValue(acceptedMediaTypes); + return acceptedMediaTypes; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandler.java new file mode 100644 index 0000000000..0730e85d23 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandler.java @@ -0,0 +1,89 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.lang.reflect.Method; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.MethodParameter; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.ModelFactory; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; + +/** + * A catch-all {@link HandlerMethodReturnValueHandler} to handle return values not handled by any other return + * value handler. + * + *

This handler should always be last in the order as {@link #supportsReturnType(MethodParameter)} always returns + * {@code true}. An attempt is made to handle the return value through a custom {@link ModelAndViewResolver}s or + * otherwise by treating it as a single model attribute. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class DefaultMethodReturnValueHandler implements HandlerMethodReturnValueHandler { + + private final ModelAndViewResolver[] customModelAndViewResolvers; + + public DefaultMethodReturnValueHandler(ModelAndViewResolver[] customResolvers) { + this.customModelAndViewResolvers = (customResolvers != null) ? customResolvers : new ModelAndViewResolver[] {}; + } + + public boolean supportsReturnType(MethodParameter returnType) { + return true; + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return false; + } + + @SuppressWarnings("unchecked") + public void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + + for (ModelAndViewResolver resolver : this.customModelAndViewResolvers) { + Class handlerType = returnType.getDeclaringClass(); + Method method = returnType.getMethod(); + ExtendedModelMap extModel = (ExtendedModelMap) mavContainer.getModel(); + ModelAndView mav = resolver.resolveModelAndView(method, handlerType, returnValue, extModel, webRequest); + if (mav != ModelAndViewResolver.UNRESOLVED) { + mavContainer.setView((V) mav.getView()); + mavContainer.setViewName(mav.getViewName()); + mavContainer.addModelAttributes(mav.getModel()); + return; + } + } + + if (returnValue == null) { + return; + } + else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) { + String name = ModelFactory.getNameForReturnValue(returnValue, returnType); + mavContainer.addModelAttribute(name, returnValue); + } + + // should not happen + throw new UnsupportedOperationException(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessor.java new file mode 100644 index 0000000000..21fdff1066 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessor.java @@ -0,0 +1,147 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.web.HttpMediaTypeNotSupportedException; +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; + +/** + * Implementation of {@link HandlerMethodArgumentResolver} and {@link HandlerMethodReturnValueHandler} + * that supports {@link HttpEntity} and {@link ResponseEntity}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor { + + public HttpEntityMethodProcessor(HttpMessageConverter... messageConverters) { + super(messageConverters); + } + + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return HttpEntity.class.equals(parameterType); + } + + public boolean supportsReturnType(MethodParameter returnType) { + Class parameterType = returnType.getParameterType(); + return HttpEntity.class.equals(parameterType) || ResponseEntity.class.equals(parameterType); + } + + public boolean usesResponseArgument(MethodParameter parameterOrReturnType) { + // only when HttpEntity or ResponseEntity is used as a return type + return parameterOrReturnType.getParameterIndex() == -1; + } + + public Object resolveArgument(MethodParameter parameter, + ModelMap model, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws IOException, HttpMediaTypeNotSupportedException { + Class paramType = getHttpEntityType(parameter); + Object body = readWithMessageConverters(webRequest, parameter, paramType); + HttpInputMessage inputMessage = createInputMessage(webRequest); + return new HttpEntity(body, inputMessage.getHeaders()); + } + + @Override + protected HttpInputMessage createInputMessage(NativeWebRequest webRequest) { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + return new ServletServerHttpRequest(servletRequest); + } + + private Class getHttpEntityType(MethodParameter methodParam) { + Assert.isAssignable(HttpEntity.class, methodParam.getParameterType()); + ParameterizedType type = (ParameterizedType) methodParam.getGenericParameterType(); + if (type.getActualTypeArguments().length == 1) { + Type typeArgument = type.getActualTypeArguments()[0]; + if (typeArgument instanceof Class) { + return (Class) typeArgument; + } + else if (typeArgument instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) typeArgument).getGenericComponentType(); + if (componentType instanceof Class) { + // Surely, there should be a nicer way to do this + Object array = Array.newInstance((Class) componentType, 0); + return array.getClass(); + } + } + } + throw new IllegalArgumentException( + "HttpEntity parameter (" + methodParam.getParameterName() + ") is not parameterized"); + + } + + public void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + if (returnValue == null) { + return; + } + Assert.isInstanceOf(HttpEntity.class, returnValue); + HttpEntity responseEntity = (HttpEntity) returnValue; + HttpOutputMessage outputMessage = createOutputMessage(webRequest); + if (responseEntity instanceof ResponseEntity) { + ((ServerHttpResponse) outputMessage).setStatusCode(((ResponseEntity) responseEntity).getStatusCode()); + } + HttpHeaders entityHeaders = responseEntity.getHeaders(); + if (!entityHeaders.isEmpty()) { + outputMessage.getHeaders().putAll(entityHeaders); + } + Object body = responseEntity.getBody(); + if (body != null) { + writeWithMessageConverters(body, createInputMessage(webRequest), outputMessage); + } + else { + // flush headers to the HttpServletResponse + outputMessage.getBody(); + } + } + + @Override + protected HttpOutputMessage createOutputMessage(NativeWebRequest webRequest) { + HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse(); + return new ServletServerHttpResponse(servletResponse); + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ModelAndViewMethodReturnValueHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ModelAndViewMethodReturnValueHandler.java new file mode 100644 index 0000000000..b38c59fbb8 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ModelAndViewMethodReturnValueHandler.java @@ -0,0 +1,52 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.ModelAndView; + +/** + * Handles {@link ModelAndView} return values. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturnValueHandler { + + public boolean supportsReturnType(MethodParameter returnType) { + return ModelAndView.class.isAssignableFrom(returnType.getParameterType()); + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return false; + } + + @SuppressWarnings("unchecked") + public void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + ModelAndView mav = (ModelAndView) returnValue; + mavContainer.setView((V) mav.getView()); + mavContainer.setViewName(mav.getViewName()); + mavContainer.addModelAttributes(mav.getModel()); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolver.java new file mode 100644 index 0000000000..518a1248d1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolver.java @@ -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.servlet.mvc.method.annotation.support; + +import java.util.Map; + +import javax.servlet.ServletException; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.annotation.support.AbstractNamedValueMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Implementation of {@link HandlerMethodArgumentResolver} that supports arguments annotated with + * {@link PathVariable @PathVariable}. + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + * @since 3.1 + */ +public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + public PathVariableMethodArgumentResolver(ConfigurableBeanFactory beanFactory) { + super(beanFactory); + } + + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(PathVariable.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class); + return new PathVariableNamedValueInfo(annotation); + } + + @Override + @SuppressWarnings("unchecked") + protected Object resolveNamedValueArgument(NativeWebRequest webRequest, MethodParameter parameter, String name) + throws Exception { + Map uriTemplateVariables = (Map) webRequest.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + return (uriTemplateVariables != null) ? uriTemplateVariables.get(name) : null; + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { + throw new IllegalStateException("Could not find @PathVariable [" + name + "] in @RequestMapping"); + } + + private static class PathVariableNamedValueInfo extends NamedValueInfo { + + private PathVariableNamedValueInfo(PathVariable annotation) { + super(annotation.value(), true, ValueConstants.DEFAULT_NONE); + } + } + + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java new file mode 100644 index 0000000000..533ea89836 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java @@ -0,0 +1,98 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.ui.ModelMap; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; +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; + +/** + * Implementation of {@link HandlerMethodArgumentResolver} and {@link HandlerMethodReturnValueHandler} that supports + * parameters annotated with {@link RequestBody} and return values annotated with {@link ResponseBody}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { + + public RequestResponseBodyMethodProcessor(HttpMessageConverter... messageConverters) { + super(messageConverters); + } + + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestBody.class); + } + + public boolean supportsReturnType(MethodParameter returnType) { + return returnType.getMethodAnnotation(ResponseBody.class) != null; + } + + public boolean usesResponseArgument(MethodParameter parameterOrReturnType) { + return parameterOrReturnType.getParameterIndex() == -1 && + parameterOrReturnType.getMethodAnnotation(ResponseBody.class) != null; + } + + public Object resolveArgument(MethodParameter parameter, + ModelMap model, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws IOException, HttpMediaTypeNotSupportedException { + return readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); + } + + @Override + protected HttpInputMessage createInputMessage(NativeWebRequest webRequest) { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + return new ServletServerHttpRequest(servletRequest); + } + + public void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) + throws IOException, HttpMediaTypeNotAcceptableException { + if (returnValue != null) { + writeWithMessageConverters(webRequest, returnValue); + } + } + + @Override + protected HttpOutputMessage createOutputMessage(NativeWebRequest webRequest) { + HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse(); + return new ServletServerHttpResponse(servletResponse); + } + +} \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolver.java new file mode 100644 index 0000000000..f49fea8320 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolver.java @@ -0,0 +1,56 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.support.CookieValueMethodArgumentResolver; +import org.springframework.web.util.WebUtils; + +/** + * A {@link CookieValueMethodArgumentResolver} for Servlet environments. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ServletCookieValueMethodArgumentResolver extends CookieValueMethodArgumentResolver { + + public ServletCookieValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory) { + super(beanFactory); + } + + @Override + protected Object resolveNamedValueArgument(NativeWebRequest webRequest, + MethodParameter parameter, + String cookieName) throws Exception { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName); + if (Cookie.class.isAssignableFrom(parameter.getParameterType())) { + return cookieValue; + } + else if (cookieValue != null) { + return getUrlPathHelper().decodeRequestString(servletRequest, cookieValue.getValue()); + } + else { + return null; + } + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java new file mode 100644 index 0000000000..8a44189da3 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java @@ -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.servlet.mvc.method.annotation.support; + +import javax.servlet.ServletRequest; + +import org.springframework.beans.BeanUtils; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.support.ModelAttributeMethodProcessor; + +/** + * A {@link ModelAttributeMethodProcessor} for Servlet environments. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor { + + /** + * Creates a {@link ServletModelAttributeMethodProcessor} instance. + * @param resolveWithoutAnnotations enable default resolution mode in which parameters 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 ServletModelAttributeMethodProcessor(boolean resolveWithoutAnnotations) { + super(resolveWithoutAnnotations); + } + + /** + * Expects the data binder to be an instance of {@link ServletRequestDataBinder}. + */ + @Override + protected void doBind(WebDataBinder binder, NativeWebRequest request) { + ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); + ((ServletRequestDataBinder) binder).bind(servletRequest); + } + +} \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolver.java new file mode 100644 index 0000000000..970d58e6af --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolver.java @@ -0,0 +1,91 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.security.Principal; +import java.util.Locale; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +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.multipart.MultipartRequest; +import org.springframework.web.servlet.support.RequestContextUtils; + +/** + * Implementation of {@link HandlerMethodArgumentResolver} that supports {@link ServletRequest} and related arguments. + * + * @author Arjen Poutsma + */ +public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver { + + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return ServletRequest.class.isAssignableFrom(parameterType) || + MultipartRequest.class.isAssignableFrom(parameterType) || + HttpSession.class.isAssignableFrom(parameterType) || Principal.class.isAssignableFrom(parameterType) || + Locale.class.equals(parameterType) || InputStream.class.isAssignableFrom(parameterType) || + Reader.class.isAssignableFrom(parameterType); + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return false; + } + + public Object resolveArgument(MethodParameter parameter, + ModelMap model, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws IOException { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + Class parameterType = parameter.getParameterType(); + + if (ServletRequest.class.isAssignableFrom(parameterType) || + MultipartRequest.class.isAssignableFrom(parameterType)) { + Object nativeRequest = webRequest.getNativeRequest(parameterType); + if (nativeRequest == null) { + throw new IllegalStateException( + "Current request is not of type [" + parameterType.getName() + "]: " + request); + } + return nativeRequest; + } + else if (HttpSession.class.isAssignableFrom(parameterType)) { + return request.getSession(); + } + else if (Principal.class.isAssignableFrom(parameterType)) { + return request.getUserPrincipal(); + } + else if (Locale.class.equals(parameterType)) { + return RequestContextUtils.getLocale(request); + } + else if (InputStream.class.isAssignableFrom(parameterType)) { + return request.getInputStream(); + } + else if (Reader.class.isAssignableFrom(parameterType)) { + return request.getReader(); + } + // should not happen + throw new UnsupportedOperationException(); + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolver.java new file mode 100644 index 0000000000..8ce50d574e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolver.java @@ -0,0 +1,73 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +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 ServletResponse} and related arguments. + * + * @author Arjen Poutsma + */ +public class ServletResponseMethodArgumentResolver implements HandlerMethodArgumentResolver { + + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return ServletResponse.class.isAssignableFrom(parameterType) || + OutputStream.class.isAssignableFrom(parameterType) || Writer.class.isAssignableFrom(parameterType); + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return true; + } + + public Object resolveArgument(MethodParameter parameter, + ModelMap model, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws IOException { + HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); + Class parameterType = parameter.getParameterType(); + + if (ServletResponse.class.isAssignableFrom(parameterType)) { + Object nativeResponse = webRequest.getNativeResponse(parameterType); + if (nativeResponse == null) { + throw new IllegalStateException( + "Current response is not of type [" + parameterType.getName() + "]: " + response); + } + return nativeResponse; + } + else if (OutputStream.class.isAssignableFrom(parameterType)) { + return response.getOutputStream(); + } + else if (Writer.class.isAssignableFrom(parameterType)) { + return response.getWriter(); + } + // should not happen + throw new UnsupportedOperationException(); + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java new file mode 100644 index 0000000000..74ba146ee8 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java @@ -0,0 +1,56 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.View; + +/** + * Handles {@link View} and view name return values. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ViewMethodReturnValueHandler implements HandlerMethodReturnValueHandler { + + public boolean supportsReturnType(MethodParameter returnType) { + Class paramType = returnType.getParameterType(); + return (View.class.isAssignableFrom(paramType) || (String.class.equals(paramType))); + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return false; + } + + @SuppressWarnings("unchecked") + public void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + if (returnValue instanceof String) { + mavContainer.setViewName((String) returnValue); + } + else { + V view = (V) returnValue; + mavContainer.setView(view); + } + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java new file mode 100644 index 0000000000..4f02a0f035 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java @@ -0,0 +1,134 @@ +/* + * 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.servlet.handler; + +import java.lang.reflect.Method; +import java.util.Comparator; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.util.UrlPathHelper; + +import static org.junit.Assert.*; + +/** + * Test for {@link AbstractHandlerMethodMapping}. + * + * @author Arjen Poutsma + */ +public class HandlerMethodMappingTests { + + private AbstractHandlerMethodMapping mapping; + + private HandlerMethod handlerMethod1; + + private HandlerMethod handlerMethod2; + + @Before + public void setUp() throws Exception { + mapping = new MyHandlerMethodMapping(); + MyHandler handler = new MyHandler(); + handlerMethod1 = new HandlerMethod(handler, "handlerMethod1"); + handlerMethod2 = new HandlerMethod(handler, "handlerMethod2"); + } + + @Test(expected = IllegalStateException.class) + public void registerDuplicates() { + mapping.registerHandlerMethod("foo", handlerMethod1); + mapping.registerHandlerMethod("foo", handlerMethod2); + } + + @Test + public void directMatch() throws Exception { + String key = "foo"; + mapping.registerHandlerMethod(key, handlerMethod1); + + HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", key)); + assertEquals(handlerMethod1, result); + } + + @Test + public void patternMatch() throws Exception { + mapping.registerHandlerMethod("/fo*", handlerMethod1); + mapping.registerHandlerMethod("/f*", handlerMethod1); + + HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo")); + assertEquals(handlerMethod1, result); + } + + @Test(expected = IllegalStateException.class) + public void ambiguousMatch() throws Exception { + mapping.registerHandlerMethod("/f?o", handlerMethod1); + mapping.registerHandlerMethod("/fo?", handlerMethod2); + + mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo")); + } + + private static class MyHandlerMethodMapping extends AbstractHandlerMethodMapping { + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + @Override + protected String getKeyForRequest(HttpServletRequest request) throws Exception { + return urlPathHelper.getLookupPathForRequest(request); + } + + @Override + protected String getMatchingKey(String pattern, HttpServletRequest request) { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + + return pathMatcher.match(pattern, lookupPath) ? pattern : null; + } + + @Override + protected String getKeyForMethod(Method method) { + String methodName = method.getName(); + return methodName.startsWith("handler") ? methodName : null; + } + + @Override + protected Comparator getKeyComparator(HttpServletRequest request) { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + + return pathMatcher.getPatternComparator(lookupPath); + } + + @Override + protected boolean isHandler(String beanName) { + return true; + } + } + + private static class MyHandler { + + @SuppressWarnings("unused") + public void handlerMethod1() { + } + + @SuppressWarnings("unused") + public void handlerMethod2() { + } + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ControllerMethodAnnotationDetectionTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ControllerMethodAnnotationDetectionTests.java new file mode 100644 index 0000000000..fc6906d862 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ControllerMethodAnnotationDetectionTests.java @@ -0,0 +1,251 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.aop.interceptor.SimpleTraceInterceptor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.TestBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.support.DefaultDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.method.HandlerMethodSelector; +import org.springframework.web.method.annotation.support.ModelAttributeMethodProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolverContainer; +import org.springframework.web.method.support.HandlerMethodReturnValueHandlerContainer; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMethodReturnValueHandler; + +/** + * Test various scenarios for detecting method-level and method parameter annotations depending + * on where they are located -- on interfaces, parent classes, in parameterized methods, or in + * combination with proxies. + * + * Note the following: + *
    + *
  • Parameterized methods cannot be used in combination with JDK dynamic proxies since the + * proxy interface does not contain the bridged methods that need to be invoked. + *
  • When using JDK dynamic proxies, the proxied interface must contain all required method + * and method parameter annotations. + *
  • Method-level annotations can be placed on super types (interface or parent class) while + * method parameter annotations must be present on the method being invoked. + *
+ * + * @author Rossen Stoyanchev + */ +@RunWith(Parameterized.class) +public class ControllerMethodAnnotationDetectionTests { + + @Parameters + public static Collection handlerTypes() { + return Arrays.asList(new Object[][] { + { new MappingIfcController(), false }, + { new MappingAbstractClassController(), false }, + { new ParameterizedIfcController(), false }, + { new MappingParameterizedIfcController(), false }, + { new MappingIfcProxyController(), true }, + { new PlainController(), true }, + { new MappingAbstractClassController(), true } + }); + } + + private Object handler; + + private boolean useAutoProxy; + + public ControllerMethodAnnotationDetectionTests(Object handler, boolean useAutoProxy) { + this.handler = handler; + this.useAutoProxy = useAutoProxy; + } + + @Test + public void invokeModelAttributeMethod() throws Exception { + ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handler, useAutoProxy); + + ModelAttribute annot = requestMappingMethod.getMethodAnnotation(ModelAttribute.class); + assertEquals("Failed to detect method annotation", "attrName", annot.value()); + + MockHttpServletRequest servletRequest = new MockHttpServletRequest(); + NativeWebRequest webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); + servletRequest.setParameter("name", "Chad"); + + ModelMap model = new ExtendedModelMap(); + ModelAndView mav = requestMappingMethod.invokeAndHandle(webRequest, model); + + Object modelAttr = mav.getModelMap().get("attrName"); + + assertEquals(TestBean.class, modelAttr.getClass()); + assertEquals("Chad", ((TestBean) modelAttr).getName()); + } + + private ServletInvocableHandlerMethod createRequestMappingMethod(Object handler, boolean useAutoProxy) { + if (useAutoProxy) { + handler = getProxyBean(handler); + } + HandlerMethodArgumentResolverContainer argResolvers = new HandlerMethodArgumentResolverContainer(); + argResolvers.registerArgumentResolver(new ModelAttributeMethodProcessor(false)); + + HandlerMethodReturnValueHandlerContainer handlers = new HandlerMethodReturnValueHandlerContainer(); + handlers.registerReturnValueHandler(new ModelAttributeMethodProcessor(false)); + handlers.registerReturnValueHandler(new DefaultMethodReturnValueHandler(null)); + + Class handlerType = ClassUtils.getUserClass(handler.getClass()); + Set methods = HandlerMethodSelector.selectMethods(handlerType, REQUEST_MAPPING_METHODS); + Method method = methods.iterator().next(); + + ServletInvocableHandlerMethod attrMethod = new ServletInvocableHandlerMethod(handler, method); + attrMethod.setArgumentResolverContainer(argResolvers); + attrMethod.setReturnValueHandlers(handlers); + attrMethod.setDataBinderFactory(new DefaultDataBinderFactory(null)); + + return attrMethod; + } + + private Object getProxyBean(Object handler) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(handler.getClass())); + + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setBeanFactory(wac.getBeanFactory()); + wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); + wac.getBeanFactory().registerSingleton("advsr", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + + wac.refresh(); + + return wac.getBean("controller"); + } + + public static MethodFilter REQUEST_MAPPING_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return AnnotationUtils.findAnnotation(method, RequestMapping.class) != null; + } + }; + + private interface MappingIfc { + @RequestMapping + @ModelAttribute("attrName") + TestBean model(TestBean input); + } + + private static class MappingIfcController implements MappingIfc { + public TestBean model(@ModelAttribute TestBean input) { + return new TestBean(input.getName()); + } + } + + private interface MappingProxyIfc { + @RequestMapping + @ModelAttribute("attrName") + TestBean model(@ModelAttribute TestBean input); + } + + private static class MappingIfcProxyController implements MappingProxyIfc { + @ModelAttribute("attrName") + public TestBean model(@ModelAttribute TestBean input) { + return new TestBean(input.getName()); + } + } + + public static abstract class MappingAbstractClass { + @RequestMapping + @ModelAttribute("attrName") + TestBean model(TestBean input) { + return new TestBean(input.getName()); + } + } + + public static class MappingAbstractClassController extends MappingAbstractClass { + public TestBean model(@ModelAttribute TestBean input) { + TestBean testBean = super.model(input); + testBean.setAge(14); + return testBean; + } + } + + public interface ParameterizedIfc { + TB model(S input); + } + + public static class ParameterizedIfcController implements ParameterizedIfc { + @RequestMapping + @ModelAttribute("attrName") + public TestBean model(@ModelAttribute TestBean input) { + return new TestBean(input.getName()); + } + } + + public interface MappingParameterizedIfc { + @RequestMapping + @ModelAttribute("attrName") + TB model(S input); + } + + public static class MappingParameterizedIfcController implements MappingParameterizedIfc { + public TestBean model(@ModelAttribute TestBean input) { + return new TestBean(input.getName()); + } + } + + public interface MappingParameterizedProxyIfc { + @RequestMapping + @ModelAttribute("attrName") + TB model(@ModelAttribute("inputName") S input); + } + + public static class MappingParameterizedProxyIfcController implements MappingParameterizedProxyIfc { + @RequestMapping + @ModelAttribute("attrName") + public TestBean model(@ModelAttribute TestBean input) { + return new TestBean(input.getName()); + } + } + + public static class PlainController { + public PlainController() { + } + + @RequestMapping + @ModelAttribute("attrName") + public TestBean model(@ModelAttribute TestBean input) { + return new TestBean(input.getName()); + } + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactoryTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactoryTests.java new file mode 100644 index 0000000000..93f3560467 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestConditionFactoryTests.java @@ -0,0 +1,155 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.util.Set; + +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.servlet.mvc.method.annotation.RequestCondition; +import org.springframework.web.servlet.mvc.method.annotation.RequestConditionFactory; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class RequestConditionFactoryTests { + + @Test + public void paramEquals() { + assertEquals(getSingleParamCondition("foo"), getSingleParamCondition("foo")); + assertFalse(getSingleParamCondition("foo").equals(getSingleParamCondition("bar"))); + assertFalse(getSingleParamCondition("foo").equals(getSingleParamCondition("FOO"))); + assertEquals(getSingleParamCondition("foo=bar"), getSingleParamCondition("foo=bar")); + assertFalse(getSingleParamCondition("foo=bar").equals(getSingleParamCondition("FOO=bar"))); + } + + @Test + public void headerEquals() { + assertEquals(getSingleHeaderCondition("foo"), getSingleHeaderCondition("foo")); + assertEquals(getSingleHeaderCondition("foo"), getSingleHeaderCondition("FOO")); + assertFalse(getSingleHeaderCondition("foo").equals(getSingleHeaderCondition("bar"))); + assertEquals(getSingleHeaderCondition("foo=bar"), getSingleHeaderCondition("foo=bar")); + assertEquals(getSingleHeaderCondition("foo=bar"), getSingleHeaderCondition("FOO=bar")); + assertEquals(getSingleHeaderCondition("content-type=text/xml"), + getSingleHeaderCondition("Content-Type=TEXT/XML")); + } + + @Test + public void headerPresent() { + RequestCondition condition = getSingleHeaderCondition("accept"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", ""); + + assertTrue(condition.match(request)); + } + + @Test + public void headerPresentNoMatch() { + RequestCondition condition = getSingleHeaderCondition("foo"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("bar", ""); + + assertFalse(condition.match(request)); + } + + @Test + public void headerNotPresent() { + RequestCondition condition = getSingleHeaderCondition("!accept"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + assertTrue(condition.match(request)); + } + + @Test + public void headerValueMatch() { + RequestCondition condition = getSingleHeaderCondition("foo=bar"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("foo", "bar"); + + assertTrue(condition.match(request)); + } + + @Test + public void headerValueNoMatch() { + RequestCondition condition = getSingleHeaderCondition("foo=bar"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("foo", "bazz"); + + assertFalse(condition.match(request)); + } + + @Test + public void headerCaseSensitiveValueMatch() { + RequestCondition condition = getSingleHeaderCondition("foo=Bar"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("foo", "bar"); + + assertFalse(condition.match(request)); + } + + @Test + public void headerValueMatchNegated() { + RequestCondition condition = getSingleHeaderCondition("foo!=bar"); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("foo", "baz"); + + assertTrue(condition.match(request)); + } + + @Test + public void mediaTypeHeaderValueMatch() { + RequestCondition condition = getSingleHeaderCondition("accept=text/html"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/html"); + + assertTrue(condition.match(request)); + } + + @Test + public void mediaTypeHeaderValueMatchNegated() { + RequestCondition condition = getSingleHeaderCondition("accept!=text/html"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "application/html"); + + assertTrue(condition.match(request)); + } + + private RequestCondition getSingleHeaderCondition(String expression) { + Set conditions = RequestConditionFactory.parseHeaders(expression); + assertEquals(1, conditions.size()); + return conditions.iterator().next(); + } + + private RequestCondition getSingleParamCondition(String expression) { + Set conditions = RequestConditionFactory.parseParams(expression); + assertEquals(1, conditions.size()); + return conditions.iterator().next(); + } + + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyComparatorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyComparatorTests.java new file mode 100644 index 0000000000..914c0f3fbd --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyComparatorTests.java @@ -0,0 +1,149 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestConditionFactory; +import org.springframework.web.servlet.mvc.method.annotation.RequestKey; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodMapping; + +/** + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public class RequestKeyComparatorTests { + + private RequestMappingHandlerMethodMapping handlerMapping; + + private MockHttpServletRequest request; + + @Before + public void setup() { + this.handlerMapping = new RequestMappingHandlerMethodMapping(); + this.request = new MockHttpServletRequest(); + } + + @Test + public void moreSpecificPatternWins() { + request.setRequestURI("/foo"); + Comparator comparator = handlerMapping.getKeyComparator(request); + RequestKey key1 = new RequestKey(asList("/fo*"), null, null, null); + RequestKey key2 = new RequestKey(asList("/foo"), null, null, null); + + assertEquals(1, comparator.compare(key1, key2)); + } + + @Test + public void equalPatterns() { + request.setRequestURI("/foo"); + Comparator comparator = handlerMapping.getKeyComparator(request); + RequestKey key1 = new RequestKey(asList("/foo*"), null, null, null); + RequestKey key2 = new RequestKey(asList("/foo*"), null, null, null); + + assertEquals(0, comparator.compare(key1, key2)); + } + + @Test + public void greaterNumberOfMatchingPatternsWins() throws Exception { + request.setRequestURI("/foo.html"); + RequestKey key1 = new RequestKey(asList("/foo", "*.jpeg"), null, null, null); + RequestKey key2 = new RequestKey(asList("/foo", "*.html"), null, null, null); + RequestKey match1 = handlerMapping.getMatchingKey(key1, request); + RequestKey match2 = handlerMapping.getMatchingKey(key2, request); + List matches = asList(match1, match2); + Collections.sort(matches, handlerMapping.getKeyComparator(request)); + + assertSame(match2.getPatterns(), matches.get(0).getPatterns()); + } + + @Test + public void oneMethodWinsOverNone() { + Comparator comparator = handlerMapping.getKeyComparator(request); + RequestKey key1 = new RequestKey(null, null, null, null); + RequestKey key2 = new RequestKey(null, asList(RequestMethod.GET), null, null); + + assertEquals(1, comparator.compare(key1, key2)); + } + + @Test + public void methodsAndParams() { + RequestKey empty = new RequestKey(null, null, null, null); + RequestKey oneMethod = new RequestKey(null, asList(RequestMethod.GET), null, null); + RequestKey oneMethodOneParam = + new RequestKey(null, asList(RequestMethod.GET), RequestConditionFactory.parseParams("foo"), null); + List list = asList(empty, oneMethod, oneMethodOneParam); + Collections.shuffle(list); + Collections.sort(list, handlerMapping.getKeyComparator(request)); + + assertEquals(oneMethodOneParam, list.get(0)); + assertEquals(oneMethod, list.get(1)); + assertEquals(empty, list.get(2)); + } + + @Test + @Ignore // TODO : remove ignore + public void acceptHeaders() { + RequestKey html = new RequestKey(null, null, null, RequestConditionFactory.parseHeaders("accept=text/html")); + RequestKey xml = new RequestKey(null, null, null, RequestConditionFactory.parseHeaders("accept=application/xml")); + RequestKey none = new RequestKey(null, null, null, null); + + request.addHeader("Accept", "application/xml, text/html"); + Comparator comparator = handlerMapping.getKeyComparator(request); + + assertTrue(comparator.compare(html, xml) > 0); + assertTrue(comparator.compare(xml, html) < 0); + assertTrue(comparator.compare(xml, none) < 0); + assertTrue(comparator.compare(none, xml) > 0); + assertTrue(comparator.compare(html, none) < 0); + assertTrue(comparator.compare(none, html) > 0); + + request = new MockHttpServletRequest(); + request.addHeader("Accept", "application/xml, text/*"); + comparator = handlerMapping.getKeyComparator(request); + + assertTrue(comparator.compare(html, xml) > 0); + assertTrue(comparator.compare(xml, html) < 0); + + request = new MockHttpServletRequest(); + request.addHeader("Accept", "application/pdf"); + comparator = handlerMapping.getKeyComparator(request); + + assertTrue(comparator.compare(html, xml) == 0); + assertTrue(comparator.compare(xml, html) == 0); + + // See SPR-7000 + request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/html;q=0.9,application/xml"); + comparator = handlerMapping.getKeyComparator(request); + + assertTrue(comparator.compare(html, xml) > 0); + assertTrue(comparator.compare(xml, html) < 0); + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyTests.java new file mode 100644 index 0000000000..3a7fb435d0 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestKeyTests.java @@ -0,0 +1,218 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestConditionFactory; +import org.springframework.web.servlet.mvc.method.annotation.RequestKey; +import org.springframework.web.util.UrlPathHelper; + +/** + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public class RequestKeyTests { + + @Test + public void equals() { + RequestKey key1 = new RequestKey(asList("/foo"), asList(GET), null, null); + RequestKey key2 = new RequestKey(asList("/foo"), asList(GET), null, null); + + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void equalsPrependSlash() { + RequestKey key1 = new RequestKey(asList("/foo"), asList(GET), null, null); + RequestKey key2 = new RequestKey(asList("foo"), asList(GET), null, null); + + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void combinePatterns() { + AntPathMatcher pathMatcher = new AntPathMatcher(); + + RequestKey key1 = createKeyFromPatterns("/t1", "/t2"); + RequestKey key2 = createKeyFromPatterns("/m1", "/m2"); + RequestKey key3 = createKeyFromPatterns("/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"); + assertEquals(key3, key1.combine(key2, pathMatcher)); + + key1 = createKeyFromPatterns("/t1"); + key2 = createKeyFromPatterns(new String[] {}); + key3 = createKeyFromPatterns("/t1"); + assertEquals(key3, key1.combine(key2, pathMatcher)); + + key1 = createKeyFromPatterns(new String[] {}); + key2 = createKeyFromPatterns("/m1"); + key3 = createKeyFromPatterns("/m1"); + assertEquals(key3, key1.combine(key2, pathMatcher)); + + key1 = createKeyFromPatterns(new String[] {}); + key2 = createKeyFromPatterns(new String[] {}); + key3 = createKeyFromPatterns("/"); + assertEquals(key3, key1.combine(key2, pathMatcher)); + + } + + @Test + public void matchPatternsToRequest() { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + PathMatcher pathMatcher = new AntPathMatcher(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + RequestKey key = new RequestKey(asList("/foo"), null, null, null); + RequestKey match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull(match); + + request = new MockHttpServletRequest("GET", "/foo/bar"); + key = new RequestKey(asList("/foo/*"), null, null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull("Pattern match", match); + + request = new MockHttpServletRequest("GET", "/foo.html"); + key = new RequestKey(asList("/foo"), null, null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull("Implicit match by extension", match); + assertEquals("Contains matched pattern", "/foo.*", match.getPatterns().iterator().next()); + + request = new MockHttpServletRequest("GET", "/foo/"); + key = new RequestKey(asList("/foo"), null, null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull("Implicit match by trailing slash", match); + assertEquals("Contains matched pattern", "/foo/", match.getPatterns().iterator().next()); + + request = new MockHttpServletRequest("GET", "/foo.html"); + key = new RequestKey(asList("/foo.jpg"), null, null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNull("Implicit match ignored if pattern has extension", match); + + request = new MockHttpServletRequest("GET", "/foo.html"); + key = new RequestKey(asList("/foo.jpg"), null, null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNull("Implicit match ignored on pattern with trailing slash", match); + } + + @Test + public void matchRequestMethods() { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + PathMatcher pathMatcher = new AntPathMatcher(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + + RequestKey key = new RequestKey(asList("/foo"), null, null, null); + RequestKey match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull("No method matches any method", match); + + key = new RequestKey(asList("/foo"), asList(GET), null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull("Exact match", match); + + key = new RequestKey(asList("/foo"), asList(POST), null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNull("No match", match); + } + + @Test + public void testMatchingKeyContent() { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + PathMatcher pathMatcher = new AntPathMatcher(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + + RequestKey key = new RequestKey(asList("/foo*", "/bar"), asList(GET, POST), null, null); + RequestKey match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + RequestKey expected = new RequestKey(asList("/foo*"), asList(GET), null, null); + + assertEquals("Matching RequestKey contains matched patterns and methods only", expected, match); + + key = new RequestKey(asList("/**", "/foo*", "/foo"), null, null, null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + expected = new RequestKey(asList("/foo", "/foo*", "/**"), null, null, null); + + assertEquals("Matched patterns are sorted with best match at the top", expected, match); + + } + + @Test + public void testParamConditions() { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + PathMatcher pathMatcher = new AntPathMatcher(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setParameter("foo", "bar"); + + RequestKey key = new RequestKey(asList("/foo"), null, RequestConditionFactory.parseParams("foo=bar"), null); + RequestKey match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull(match); + + key = new RequestKey(asList("/foo"), null, RequestConditionFactory.parseParams("foo!=bar"), null); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNull(match); + } + + @Test + public void testHeaderConditions() { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + PathMatcher pathMatcher = new AntPathMatcher(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addHeader("foo", "bar"); + + RequestKey key = new RequestKey(asList("/foo"), null, null, RequestConditionFactory.parseHeaders("foo=bar")); + RequestKey match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNotNull(match); + + key = new RequestKey(asList("/foo"), null, null, RequestConditionFactory.parseHeaders("foo!=bar")); + match = key.getMatchingKey(request, pathMatcher, urlPathHelper); + + assertNull(match); + } + + @Test + public void testCreateFromServletRequest() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + RequestKey key = RequestKey.createFromServletRequest(request, new UrlPathHelper()); + assertEquals(new RequestKey(asList("/foo"), asList(RequestMethod.GET), null, null), key); + } + + private RequestKey createKeyFromPatterns(String... patterns) { + return new RequestKey(asList(patterns), null, null, null); + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java new file mode 100644 index 0000000000..84f43b05ee --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -0,0 +1,349 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.awt.Color; +import java.lang.reflect.Method; +import java.security.Principal; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.TestBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * A test fixture for higher-level {@link RequestMappingHandlerAdapter} tests. + * + *

The aim here is not to test {@link RequestMappingHandlerAdapter} itself nor to exercise + * every {@link Controller @Controller} method feature but to have a place to try any feature + * related to {@link Controller @Controller} invocations. Preferably actual tests should be + * added to the components that provide that respective functionality. + * + *

The following integration tests for detecting annotations on super types, parameterized + * methods, and proxies may also be of interest: + *

    + *
  • {@link RequestMappingHandlerMethodDetectionTests} + *
  • {@link ControllerMethodAnnotationDetectionTests} + *
+ * + * @author Rossen Stoyanchev + */ +public class RequestMappingHandlerAdapterIntegrationTests { + + private RequestMappingHandlerAdapter handlerAdapter; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setup() throws Exception { + ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + bindingInitializer.setValidator(new StubValidator()); + + this.handlerAdapter = new RequestMappingHandlerAdapter(); + this.handlerAdapter.setWebBindingInitializer(bindingInitializer); + this.handlerAdapter.setCustomArgumentResolvers(new WebArgumentResolver[] { new ColorArgumentResolver() }); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.refresh(); + + this.handlerAdapter.setApplicationContext(context); + this.handlerAdapter.setBeanFactory(context.getBeanFactory()); + this.handlerAdapter.afterPropertiesSet(); + + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + + // Expose request to the current thread (for SpEL expressions) + RequestContextHolder.setRequestAttributes(new ServletWebRequest(request)); + } + + @After + public void teardown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void handleMvc() throws Exception { + Class[] paramTypes = new Class[] { int.class, String.class, String.class, String.class, Map.class, + Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class, + Color.class, HttpServletRequest.class, HttpServletResponse.class, User.class, OtherUser.class, + Model.class }; + + /* URI template vars (see RequestMappingHandlerMethodMapping) */ + Map uriTemplateVars = new HashMap(); + uriTemplateVars.put("pathvar", "pathvarValue"); + request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + + Date date = new GregorianCalendar(2011, Calendar.MARCH, 16).getTime(); + String formattedDate = "2011.03.16"; + + System.setProperty("systemHeader", "systemHeaderValue"); + + request.setCookies(new Cookie("cookie", "99")); + request.addHeader("header", "headerValue"); + request.addHeader("anotherHeader", "anotherHeaderValue"); + request.addParameter("datePattern", "yyyy.MM.dd"); + request.addParameter("dateParam", formattedDate); + request.addParameter("paramByConvention", "paramByConventionValue"); + request.addParameter("age", "25"); + request.setContextPath("/contextPath"); + + request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.setContent("Hello World".getBytes("UTF-8")); + + request.setUserPrincipal(new User()); + + HandlerMethod handlerMethod = handlerMethod(new RequestMappingHandler(), "handleMvc", paramTypes); + ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); + ModelMap model = mav.getModelMap(); + + assertEquals("viewName", mav.getViewName()); + assertEquals(99, model.get("cookie")); + assertEquals("pathvarValue", model.get("pathvar")); + assertEquals("headerValue", model.get("header")); + assertEquals(date, model.get("dateParam")); + + Map map = (Map) model.get("headerMap"); + assertEquals("headerValue", map.get("header")); + assertEquals("anotherHeaderValue", map.get("anotherHeader")); + assertEquals("systemHeaderValue", model.get("systemHeader")); + + map = (Map) model.get("paramMap"); + assertEquals(formattedDate, map.get("dateParam")); + assertEquals("paramByConventionValue", map.get("paramByConvention")); + + assertEquals("/contextPath", model.get("value")); + + TestBean modelAttr = (TestBean) model.get("modelAttr"); + assertEquals(25, modelAttr.getAge()); + assertEquals("Set by model method [modelAttr]", modelAttr.getName()); + assertSame(modelAttr, request.getSession().getAttribute("modelAttr")); + + BindingResult bindingResult = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "modelAttr"); + assertSame(modelAttr, bindingResult.getTarget()); + assertEquals(1, bindingResult.getErrorCount()); + + String conventionAttrName = "testBean"; + TestBean modelAttrByConvention = (TestBean) model.get(conventionAttrName); + assertEquals(25, modelAttrByConvention.getAge()); + assertEquals("Set by model method [modelAttrByConvention]", modelAttrByConvention.getName()); + assertSame(modelAttrByConvention, request.getSession().getAttribute(conventionAttrName)); + + bindingResult = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + conventionAttrName); + assertSame(modelAttrByConvention, bindingResult.getTarget()); + + assertTrue(model.get("customArg") instanceof Color); + assertEquals(User.class, model.get("user").getClass()); + assertEquals(OtherUser.class, model.get("otherUser").getClass()); + } + + @Test + public void handleRequestBody() throws Exception { + Class[] paramTypes = new Class[] { byte[].class }; + + request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.setContent("Hello Server".getBytes("UTF-8")); + + HandlerMethod handlerMethod = handlerMethod(new RequestMappingHandler(), "handleRequestBody", paramTypes); + + ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); + + assertNull(mav); + assertEquals("Handled requestBody=[Hello Server]", new String(response.getContentAsByteArray(), "UTF-8")); + assertEquals(HttpStatus.ACCEPTED.value(), response.getStatus()); + } + + @Test + public void handleHttpEntity() throws Exception { + Class[] paramTypes = new Class[] { HttpEntity.class }; + + request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.setContent("Hello Server".getBytes("UTF-8")); + + HandlerMethod handlerMethod = handlerMethod(new RequestMappingHandler(), "handleHttpEntity", paramTypes); + + ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); + + assertNull(mav); + assertEquals(HttpStatus.ACCEPTED.value(), response.getStatus()); + assertEquals("Handled requestBody=[Hello Server]", new String(response.getContentAsByteArray(), "UTF-8")); + assertEquals("headerValue", response.getHeader("header")); + } + + private HandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) throws Exception { + Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); + return new InvocableHandlerMethod(handler, method); + } + + @SessionAttributes(types=TestBean.class) + private static class RequestMappingHandler { + + @SuppressWarnings("unused") + @InitBinder("dateParam") + public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String datePattern) { + SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern); + dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + @SuppressWarnings("unused") + @ModelAttribute + public void model(Model model) { + TestBean modelAttr = new TestBean(); + modelAttr.setName("Set by model method [modelAttr]"); + model.addAttribute("modelAttr", modelAttr); + + modelAttr = new TestBean(); + modelAttr.setName("Set by model method [modelAttrByConvention]"); + model.addAttribute(modelAttr); + + model.addAttribute(new OtherUser()); + } + + @SuppressWarnings("unused") + public String handleMvc( + @CookieValue("cookie") int cookie, + @PathVariable("pathvar") String pathvar, + @RequestHeader("header") String header, + @RequestHeader(defaultValue="#{systemProperties.systemHeader}") String systemHeader, + @RequestHeader Map headerMap, + @RequestParam("dateParam") Date dateParam, + @RequestParam Map paramMap, + String paramByConvention, + @Value("#{request.contextPath}") String value, + @ModelAttribute("modelAttr") @Valid TestBean modelAttr, + Errors errors, + TestBean modelAttrByConvention, + Color customArg, + HttpServletRequest request, + HttpServletResponse response, + User user, + @ModelAttribute OtherUser otherUser, + Model model) throws Exception { + + model.addAttribute("cookie", cookie).addAttribute("pathvar", pathvar).addAttribute("header", header) + .addAttribute("systemHeader", systemHeader).addAttribute("headerMap", headerMap) + .addAttribute("dateParam", dateParam).addAttribute("paramMap", paramMap) + .addAttribute("paramByConvention", paramByConvention).addAttribute("value", value) + .addAttribute("customArg", customArg).addAttribute(user); + + assertNotNull(request); + assertNotNull(response); + + return "viewName"; + } + + @SuppressWarnings("unused") + @ResponseStatus(value=HttpStatus.ACCEPTED) + @ResponseBody + public String handleRequestBody(@RequestBody byte[] bytes) throws Exception { + String requestBody = new String(bytes, "UTF-8"); + return "Handled requestBody=[" + requestBody + "]"; + } + + @SuppressWarnings("unused") + public ResponseEntity handleHttpEntity(HttpEntity httpEntity) throws Exception { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set("header", "headerValue"); + String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]"; + return new ResponseEntity(responseBody, responseHeaders, HttpStatus.ACCEPTED); + } + } + + private static class StubValidator implements Validator { + public boolean supports(Class clazz) { + return true; + } + + public void validate(Object target, Errors errors) { + errors.reject("error"); + } + } + + private static class ColorArgumentResolver implements WebArgumentResolver { + public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception { + return new Color(0); + } + } + + private static class User implements Principal { + public String getName() { + return "user"; + } + } + + private static class OtherUser implements Principal { + public String getName() { + return "other user"; + } + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java new file mode 100644 index 0000000000..a6ec02c171 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -0,0 +1,97 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * Test fixture for {@link RequestMappingHandlerAdapter} unit tests. + * + * The tests in this class focus on {@link RequestMappingHandlerAdapter} functionality exclusively. + * Also see {@link RequestMappingHandlerAdapterIntegrationTests} for higher-level tests invoking + * {@link Controller @Controller} methods. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingHandlerAdapterTests { + + private RequestMappingHandlerAdapter handlerAdapter; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setup() throws Exception { + this.handlerAdapter = new RequestMappingHandlerAdapter(); + this.handlerAdapter.setApplicationContext(new GenericWebApplicationContext()); + this.handlerAdapter.afterPropertiesSet(); + + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + public void cacheControlWithoutSessionAttributes() throws Exception { + handlerAdapter.setCacheSeconds(100); + handlerAdapter.handle(request, response, handlerMethod(new SimpleHandler(), "handle")); + + assertTrue(response.getHeader("Cache-Control").toString().contains("max-age")); + } + + @Test + public void cacheControlWithSessionAttributes() throws Exception { + handlerAdapter.setCacheSeconds(100); + handlerAdapter.handle(request, response, handlerMethod(new SessionAttributeHandler(), "handle")); + + assertEquals("no-cache", response.getHeader("Cache-Control")); + } + + private HandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) throws Exception { + Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); + return new InvocableHandlerMethod(handler, method); + } + + private static class SimpleHandler { + + @SuppressWarnings("unused") + public void handle() { + } + } + + @SessionAttributes("attr1") + private static class SessionAttributeHandler { + + @SuppressWarnings("unused") + public void handle() { + } + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodDetectionTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodDetectionTests.java new file mode 100644 index 0000000000..37f3d8d33a --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodDetectionTests.java @@ -0,0 +1,167 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.Collection; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.aop.interceptor.SimpleTraceInterceptor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.TestBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodMapping; + +/** + * Test various scenarios for detecting handler methods depending on where @RequestMapping annotations + * are located -- super types, parameterized methods, or in combination with proxies. + * + * Note the following: + *
    + *
  • Parameterized methods cannot be used in combination with JDK dynamic proxies since the + * proxy interface does not contain the bridged methods that need to be invoked. + *
  • When using JDK dynamic proxies, the proxied interface must contain all required annotations. + *
  • Method-level annotations can be placed on parent classes or interfaces. + *
+ * + * @author Rossen Stoyanchev + */ +@RunWith(Parameterized.class) +public class RequestMappingHandlerMethodDetectionTests { + + @Parameters + public static Collection handlerTypes() { + return Arrays.asList(new Object[][] { + { new MappingInterfaceController(), false}, + { new MappingAbstractClassController(), false}, + { new ParameterizedInterfaceController(), false }, + { new MappingParameterizedInterfaceController(), false }, + { new MappingAbstractClassController(), true}, + { new PlainController(), true} + }); + } + + private Object handler; + + private boolean useAutoProxy; + + public RequestMappingHandlerMethodDetectionTests(Object handler, boolean useAutoProxy) { + this.handler = handler; + this.useAutoProxy = useAutoProxy; + } + + @Test + public void detectAndMapHandlerMethod() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/handle"); + + TestRequestMappingHandlerMethodMapping mapping = createHandlerMapping(handler.getClass(), useAutoProxy); + HandlerMethod handlerMethod = mapping.getHandlerInternal(request); + + assertNotNull("Failed to detect and map @RequestMapping handler method", handlerMethod); + } + + private TestRequestMappingHandlerMethodMapping createHandlerMapping(Class controllerType, boolean useAutoProxy) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(controllerType)); + if (useAutoProxy) { + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setBeanFactory(wac.getBeanFactory()); + wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); + wac.getBeanFactory().registerSingleton("advsr", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + } + + TestRequestMappingHandlerMethodMapping mapping = new TestRequestMappingHandlerMethodMapping(); + mapping.setApplicationContext(wac); + + return mapping; + } + + public static class TestRequestMappingHandlerMethodMapping extends RequestMappingHandlerMethodMapping { + public HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + return super.getHandlerInternal(request); + } + } + + @Controller + public interface MappingInterface { + @RequestMapping(value="/handle", method = RequestMethod.GET) + void handle(); + } + + public static class MappingInterfaceController implements MappingInterface { + public void handle() { + } + } + + @Controller + public static abstract class MappingAbstractClass { + @RequestMapping(value = "/handle", method = RequestMethod.GET) + public abstract void handle(); + } + + public static class MappingAbstractClassController extends MappingAbstractClass { + public void handle() { + } + } + + @Controller + public interface ParameterizedInterface { + void handle(T object); + } + + public static class ParameterizedInterfaceController implements ParameterizedInterface { + @RequestMapping(value = "/handle", method = RequestMethod.GET) + public void handle(TestBean object) { + } + } + + @Controller + public interface MappingParameterizedInterface { + @RequestMapping(value = "/handle", method = RequestMethod.GET) + void handle(T object); + } + + public static class MappingParameterizedInterfaceController implements MappingParameterizedInterface { + public void handle(TestBean object) { + } + } + + @Controller + public static class PlainController { + public PlainController() { + } + + @RequestMapping(value = "/handle", method = RequestMethod.GET) + public void handle() { + } + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolverTests.java new file mode 100644 index 0000000000..76a27042a5 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodExceptionResolverTests.java @@ -0,0 +1,239 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.BindException; +import java.net.SocketException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodExceptionResolver; + +/** + * Test fixture for {@link RequestMappingHandlerMethodExceptionResolver} unit tests. + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + * @since 3.1 + */ +public class RequestMappingHandlerMethodExceptionResolverTests { + + private RequestMappingHandlerMethodExceptionResolver exceptionResolver; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setUp() throws Exception { + exceptionResolver = new RequestMappingHandlerMethodExceptionResolver(); + exceptionResolver.afterPropertiesSet(); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + request.setMethod("GET"); + } + + @Test + public void simpleWithIOException() throws NoSuchMethodException { + IOException ex = new IOException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertEquals("Invalid view name returned", "X:IOException", mav.getViewName()); + assertEquals("Invalid status code returned", 500, response.getStatus()); + } + + @Test + public void simpleWithSocketException() throws NoSuchMethodException { + SocketException ex = new SocketException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertEquals("Invalid view name returned", "Y:SocketException", mav.getViewName()); + assertEquals("Invalid status code returned", 406, response.getStatus()); + assertEquals("Invalid status reason returned", "This is simply unacceptable!", response.getErrorMessage()); + } + + @Test + public void simpleWithFileNotFoundException() throws NoSuchMethodException { + FileNotFoundException ex = new FileNotFoundException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertEquals("Invalid view name returned", "X:FileNotFoundException", mav.getViewName()); + assertEquals("Invalid status code returned", 500, response.getStatus()); + } + + @Test + public void simpleWithBindException() throws NoSuchMethodException { + BindException ex = new BindException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertEquals("Invalid view name returned", "Y:BindException", mav.getViewName()); + assertEquals("Invalid status code returned", 406, response.getStatus()); + } + + @Test + public void inherited() throws NoSuchMethodException { + IOException ex = new IOException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new InheritedController(), "handle"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertEquals("Invalid view name returned", "GenericError", mav.getViewName()); + assertEquals("Invalid status code returned", 500, response.getStatus()); + } + + @Test(expected = IllegalStateException.class) + public void ambiguous() throws NoSuchMethodException { + IllegalArgumentException ex = new IllegalArgumentException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new AmbiguousController(), "handle"); + exceptionResolver.resolveException(request, response, handlerMethod, ex); + } + + @Test + public void noModelAndView() throws UnsupportedEncodingException, NoSuchMethodException { + IllegalArgumentException ex = new IllegalArgumentException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new NoMAVReturningController(), "handle"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertTrue("ModelAndView not empty", mav.isEmpty()); + assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString()); + } + + @Test + public void responseBody() throws UnsupportedEncodingException, NoSuchMethodException { + IllegalArgumentException ex = new IllegalArgumentException(); + HandlerMethod handlerMethod = new InvocableHandlerMethod(new ResponseBodyController(), "handle"); + request.addHeader("Accept", "text/plain"); + ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex); + assertNotNull("No ModelAndView returned", mav); + assertTrue("ModelAndView not empty", mav.isEmpty()); + assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString()); + } + + + @Controller + private static class SimpleController { + + @SuppressWarnings("unused") + public void handle() {} + + @SuppressWarnings("unused") + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public String handleIOException(IOException ex, HttpServletRequest request) { + return "X:" + ClassUtils.getShortName(ex.getClass()); + } + + @SuppressWarnings("unused") + @ExceptionHandler(SocketException.class) + @ResponseStatus(value = HttpStatus.NOT_ACCEPTABLE, reason = "This is simply unacceptable!") + public String handleSocketException(Exception ex, HttpServletResponse response) { + return "Y:" + ClassUtils.getShortName(ex.getClass()); + } + + @SuppressWarnings("unused") + @ExceptionHandler(IllegalArgumentException.class) + public String handleIllegalArgumentException(Exception ex) { + return ClassUtils.getShortName(ex.getClass()); + } + } + + + @Controller + private static class InheritedController extends SimpleController { + + @Override + public String handleIOException(IOException ex, HttpServletRequest request) { + return "GenericError"; + } + } + + + @Controller + private static class AmbiguousController { + + @SuppressWarnings("unused") + public void handle() {} + + @SuppressWarnings("unused") + @ExceptionHandler({BindException.class, IllegalArgumentException.class}) + public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response) + throws IOException { + return ClassUtils.getShortName(ex.getClass()); + } + + @SuppressWarnings("unused") + @ExceptionHandler + public String handle2(IllegalArgumentException ex) { + return ClassUtils.getShortName(ex.getClass()); + } + } + + + @Controller + private static class NoMAVReturningController { + + @SuppressWarnings("unused") + public void handle() {} + + @SuppressWarnings("unused") + @ExceptionHandler(Exception.class) + public void handle(Exception ex, Writer writer) throws IOException { + writer.write(ClassUtils.getShortName(ex.getClass())); + } + } + + + @Controller + private static class ResponseBodyController { + + @SuppressWarnings("unused") + public void handle() {} + + @SuppressWarnings("unused") + @ExceptionHandler(Exception.class) + @ResponseBody + public String handle(Exception ex) { + return ClassUtils.getShortName(ex.getClass()); + } + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMappingTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMappingTests.java new file mode 100644 index 0000000000..6c14c3f4b4 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMethodMappingTests.java @@ -0,0 +1,153 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.stereotype.Controller; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.servlet.mvc.method.annotation.RequestKey; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodMapping; + +/** + * @author Arjen Poutsma + */ +public class RequestMappingHandlerMethodMappingTests { + + private MyRequestMappingHandlerMethodMapping mapping; + + private MyHandler handler; + + private HandlerMethod fooMethod; + + private HandlerMethod barMethod; + + @Before + public void setUp() throws Exception { + handler = new MyHandler(); + fooMethod = new HandlerMethod(handler, "foo"); + barMethod = new HandlerMethod(handler, "bar"); + + StaticApplicationContext context = new StaticApplicationContext(); + context.registerSingleton("handler", handler.getClass()); + + mapping = new MyRequestMappingHandlerMethodMapping(); + mapping.setApplicationContext(context); + } + + @Test + public void directMatch() throws Exception { + HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo")); + assertEquals(fooMethod.getMethod(), result.getMethod()); + } + + @Test + public void globMatch() throws Exception { + HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/bar")); + assertEquals(barMethod.getMethod(), result.getMethod()); + } + + @Test + public void methodNotAllowed() throws Exception { + try { + mapping.getHandlerInternal(new MockHttpServletRequest("POST", "/bar")); + fail("HttpRequestMethodNotSupportedException expected"); + } + catch (HttpRequestMethodNotSupportedException ex) { + assertArrayEquals("Invalid supported methods", new String[]{"GET", "HEAD"}, ex.getSupportedMethods()); + } + } + + @Test + public void uriTemplateVariables() { + RequestKey key = new RequestKey(Arrays.asList("/{path1}/{path2}"), null, null, null); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); + + mapping.handleMatch(key, request); + + @SuppressWarnings("unchecked") + Map actual = (Map) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + + assertNotNull(actual); + assertEquals("1", actual.get("path1")); + assertEquals("2", actual.get("path2")); + } + + @Test + public void mappedInterceptors() { + String path = "/handle"; + HandlerInterceptor interceptor = new HandlerInterceptorAdapter() {}; + MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] {path}, interceptor); + + MyRequestMappingHandlerMethodMapping mapping = new MyRequestMappingHandlerMethodMapping(); + mapping.setMappedInterceptors(new MappedInterceptor[] { mappedInterceptor }); + + HandlerExecutionChain chain = mapping.getHandlerExecutionChain(handler, new MockHttpServletRequest("GET", path)); + assertNotNull(chain.getInterceptors()); + assertSame(interceptor, chain.getInterceptors()[0]); + + chain = mapping.getHandlerExecutionChain(handler, new MockHttpServletRequest("GET", "/invalid")); + assertNull(chain.getInterceptors()); + } + + private static class MyRequestMappingHandlerMethodMapping extends RequestMappingHandlerMethodMapping { + + @Override + public HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + return super.getHandlerInternal(request); + } + } + + @Controller + private static class MyHandler { + + @SuppressWarnings("unused") + @RequestMapping(value = "/foo", method = RequestMethod.GET) + public void foo() { + } + + @SuppressWarnings("unused") + @RequestMapping(value = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD }) + public void bar() { + } + + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java new file mode 100644 index 0000000000..65729f624d --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java @@ -0,0 +1,73 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.HandlerMethodReturnValueHandlerContainer; +import org.springframework.web.method.support.StubReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; + +/** + * Test fixture for {@link ServletInvocableHandlerMethod} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ServletInvocableHandlerMethodTests { + + private ServletWebRequest webRequest; + + private MockHttpServletResponse response; + + @Before + public void setUp() throws Exception { + response = new MockHttpServletResponse(); + this.webRequest = new ServletWebRequest(new MockHttpServletRequest(), response); + } + + @Test + public void setResponseStatus() throws Exception { + HandlerMethodReturnValueHandlerContainer handlers = new HandlerMethodReturnValueHandlerContainer(); + handlers.registerReturnValueHandler(new StubReturnValueHandler(void.class, false)); + + Method method = Handler.class.getDeclaredMethod("responseStatus"); + ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(new Handler(), method); + handlerMethod.setReturnValueHandlers(handlers); + + handlerMethod.invokeAndHandle(webRequest, null); + + assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); + assertEquals("400 Bad Request", response.getErrorMessage()); + } + + private static class Handler { + + @SuppressWarnings("unused") + @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "400 Bad Request") + public void responseStatus() { + } + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandlerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandlerTests.java new file mode 100644 index 0000000000..652a8062e2 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/DefaultMethodReturnValueHandlerTests.java @@ -0,0 +1,81 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMethodReturnValueHandler; + +/** + * Test fixture for {@link DefaultMethodReturnValueHandler} unit tests. + * + * @author Rossen Stoyanchev + */ +public class DefaultMethodReturnValueHandlerTests { + + private DefaultMethodReturnValueHandler handler; + + private ServletWebRequest webRequest; + + private ModelAndViewContainer mavContainer; + + @Before + public void setUp() { + this.handler = new DefaultMethodReturnValueHandler(null); + this.mavContainer = new ModelAndViewContainer(new ExtendedModelMap()); + this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + } + + @Test(expected=UnsupportedOperationException.class) + public void returnSimpleType() throws Exception { + handler.handleReturnValue(55, createMethodParam("simpleType"), mavContainer, webRequest); + } + + @Test + public void returnVoid() throws Exception { + handler.handleReturnValue(null, null, mavContainer, webRequest); + assertNull(mavContainer.getView()); + assertNull(mavContainer.getViewName()); + assertTrue(mavContainer.getModel().isEmpty()); + } + + private MethodParameter createMethodParam(String methodName) throws Exception { + Method method = getClass().getDeclaredMethod(methodName); + return new MethodParameter(method, -1); + } + + @SuppressWarnings("unused") + private int simpleType() { + return 0; + } + + @SuppressWarnings("unused") + private void voidReturnValue() { + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessorTests.java new file mode 100644 index 0000000000..b84ab0508a --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/HttpEntityMethodProcessorTests.java @@ -0,0 +1,243 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor; + +/** + * @author Arjen Poutsma + */ +public class HttpEntityMethodProcessorTests { + + private HttpEntityMethodProcessor processor; + + private HttpMessageConverter messageConverter; + + private MethodParameter httpEntityParam; + + private MethodParameter responseEntityReturnValue; + + private MethodParameter responseEntityParameter; + + private MethodParameter intReturnValue; + + private ServletWebRequest request; + + private MockHttpServletRequest servletRequest; + + private MockHttpServletResponse servletResponse; + + private MethodParameter httpEntityReturnValue; + + private MethodParameter intParameter; + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception { + messageConverter = createMock(HttpMessageConverter.class); + processor = new HttpEntityMethodProcessor(messageConverter); + + Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class, Integer.TYPE); + httpEntityParam = new MethodParameter(handle1, 0); + responseEntityParameter = new MethodParameter(handle1, 1); + intParameter = new MethodParameter(handle1, 2); + responseEntityReturnValue = new MethodParameter(handle1, -1); + + Method handle2 = getClass().getMethod("handle2", HttpEntity.class); + httpEntityReturnValue = new MethodParameter(handle2, -1); + + Method other = getClass().getMethod("otherMethod"); + intReturnValue = new MethodParameter(other, -1); + + servletRequest = new MockHttpServletRequest(); + servletResponse = new MockHttpServletResponse(); + request = new ServletWebRequest(servletRequest, servletResponse); + } + + @Test + public void supportsParameter() { + assertTrue("HttpEntity parameter not supported", processor.supportsParameter(httpEntityParam)); + assertFalse("ResponseEntity parameter supported", processor.supportsParameter(responseEntityParameter)); + assertFalse("non-entity parameter supported", processor.supportsParameter(intParameter)); + } + + @Test + public void supportsReturnType() { + assertTrue("ResponseEntity return type not supported", processor.supportsReturnType(responseEntityReturnValue)); + assertTrue("HttpEntity return type not supported", processor.supportsReturnType(httpEntityReturnValue)); + assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(intReturnValue)); + } + + @Test + public void usesResponseArgument() { + assertFalse("HttpEntity parameter uses response argument", processor.usesResponseArgument(httpEntityParam)); + assertTrue("ResponseBody return type does not use response argument", + processor.usesResponseArgument(responseEntityReturnValue)); + assertTrue("HttpEntity return type does not use response argument", + processor.usesResponseArgument(httpEntityReturnValue)); + } + + @Test + @SuppressWarnings("unchecked") + public void resolveArgument() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + String expected = "Foo"; + + servletRequest.addHeader("Content-Type", contentType.toString()); + + expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType)); + expect(messageConverter.canRead(String.class, contentType)).andReturn(true); + expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(expected); + + replay(messageConverter); + + HttpEntity result = (HttpEntity) processor.resolveArgument(httpEntityParam, null, request, null); + assertEquals("Invalid argument", expected, result.getBody()); + + verify(messageConverter); + } + + @Test(expected = HttpMediaTypeNotSupportedException.class) + public void resolveArgumentNotReadable() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + + servletRequest.addHeader("Content-Type", contentType.toString()); + + expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType)); + expect(messageConverter.canRead(String.class, contentType)).andReturn(false); + + replay(messageConverter); + + processor.resolveArgument(httpEntityParam, null, request, null); + + verify(messageConverter); + } + + @Test(expected = HttpMediaTypeNotSupportedException.class) + public void resolveArgumentNoContentType() throws Exception { + processor.resolveArgument(httpEntityParam, null, request, null); + } + + @Test + public void handleReturnValue() throws Exception { + MediaType accepted = MediaType.TEXT_PLAIN; + String s = "Foo"; + ResponseEntity returnValue = new ResponseEntity(s, HttpStatus.OK); + + servletRequest.addHeader("Accept", accepted.toString()); + + expect(messageConverter.canWrite(String.class, accepted)).andReturn(true); + messageConverter.write(eq(s), eq(accepted), isA(HttpOutputMessage.class)); + + replay(messageConverter); + + processor.handleReturnValue(returnValue, responseEntityReturnValue, null, request); + + verify(messageConverter); + } + + @Test(expected = HttpMediaTypeNotAcceptableException.class) + public void handleReturnValueNotAcceptable() throws Exception { + MediaType accepted = MediaType.TEXT_PLAIN; + String s = "Foo"; + ResponseEntity returnValue = new ResponseEntity(s, HttpStatus.OK); + + servletRequest.addHeader("Accept", accepted.toString()); + + expect(messageConverter.canWrite(String.class, accepted)).andReturn(false); + expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM)); + + replay(messageConverter); + + processor.handleReturnValue(returnValue, responseEntityReturnValue, null, request); + + verify(messageConverter); + } + + @Test + public void responseHeaderNoBody() throws Exception { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set("header", "headerValue"); + ResponseEntity returnValue = new ResponseEntity(responseHeaders, HttpStatus.ACCEPTED); + + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(new StringHttpMessageConverter()); + processor.handleReturnValue(returnValue, responseEntityReturnValue, null, request); + + assertEquals("headerValue", servletResponse.getHeader("header")); + } + + @Test + public void responseHeaderAndBody() throws Exception { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set("header", "headerValue"); + ResponseEntity returnValue = new ResponseEntity("body", responseHeaders, HttpStatus.ACCEPTED); + + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(new StringHttpMessageConverter()); + processor.handleReturnValue(returnValue, responseEntityReturnValue, null, request); + + assertEquals("headerValue", servletResponse.getHeader("header")); + } + + public ResponseEntity handle1(HttpEntity httpEntity, ResponseEntity responseEntity, int i) { + return responseEntity; + } + + public HttpEntity handle2(HttpEntity entity) { + return entity; + } + + public HttpEntity handle3() { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set("header", "headerValue"); + return new ResponseEntity(responseHeaders, HttpStatus.OK); + } + + public int otherMethod() { + return 42; + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolverTests.java new file mode 100644 index 0000000000..45440b5ac5 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/PathVariableMethodArgumentResolverTests.java @@ -0,0 +1,97 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.support.PathVariableMethodArgumentResolver; + +/** + * @author Rossen Stoyanchev + */ +public class PathVariableMethodArgumentResolverTests { + + private PathVariableMethodArgumentResolver resolver; + + private MethodParameter pathVarParam; + + private MethodParameter stringParam; + + private MockHttpServletRequest servletRequest; + + private ServletWebRequest webRequest; + + @Before + public void setUp() throws Exception { + resolver = new PathVariableMethodArgumentResolver(null); + Method method = getClass().getMethod("handle", String.class, String.class); + pathVarParam = new MethodParameter(method, 0); + stringParam = new MethodParameter(method, 1); + + servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); + } + + @Test + public void usesResponseArgument() { + assertFalse(resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue("Parameter with @PathVariable annotation", resolver.supportsParameter(pathVarParam)); + assertFalse("Parameter without @PathVariable annotation", resolver.supportsParameter(stringParam)); + } + + @Test + public void resolveStringArgument() throws Exception { + String expected = "foo"; + + Map uriTemplateVars = new HashMap(); + uriTemplateVars.put("name", expected); + servletRequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + + String result = (String) resolver.resolveArgument(pathVarParam, null, webRequest, null); + assertEquals(expected, result); + } + + @Test(expected = IllegalStateException.class) + public void handleMissingValue() throws Exception { + resolver.resolveArgument(pathVarParam, null, webRequest, null); + fail("Unresolved path variable should lead to exception."); + } + + public void handle(@PathVariable(value = "name") String param1, String param2) { + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java new file mode 100644 index 0000000000..0f2940e69a --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java @@ -0,0 +1,190 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor; + +/** + * @author Arjen Poutsma + */ +public class RequestResponseBodyMethodProcessorTests { + + private RequestResponseBodyMethodProcessor processor; + + private HttpMessageConverter messageConverter; + + private MethodParameter stringParameter; + + private MethodParameter stringReturnValue; + + private MethodParameter intParameter; + + private MethodParameter intReturnValue; + + private NativeWebRequest webRequest; + + private MockHttpServletRequest servletRequest; + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception { + messageConverter = createMock(HttpMessageConverter.class); + processor = new RequestResponseBodyMethodProcessor(messageConverter); + Method handle = getClass().getMethod("handle", String.class, Integer.TYPE); + stringParameter = new MethodParameter(handle, 0); + intParameter = new MethodParameter(handle, 1); + stringReturnValue = new MethodParameter(handle, -1); + Method other = getClass().getMethod("otherMethod"); + intReturnValue = new MethodParameter(other, -1); + + servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); + } + + @Test + public void supportsParameter() { + assertTrue("RequestBody parameter not supported", processor.supportsParameter(stringParameter)); + assertFalse("non-RequestBody parameter supported", processor.supportsParameter(intParameter)); + } + + @Test + public void supportsReturnType() { + assertTrue("ResponseBody return type not supported", processor.supportsReturnType(stringReturnValue)); + assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(intReturnValue)); + } + + @Test + public void usesResponseArgument() { + assertFalse("RequestBody parameter uses response argument", processor.usesResponseArgument(stringParameter)); + assertTrue("ResponseBody return type does not use response argument", + processor.usesResponseArgument(stringReturnValue)); + } + + @Test + public void resolveArgument() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + String expected = "Foo"; + + servletRequest.addHeader("Content-Type", contentType.toString()); + + expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType)); + expect(messageConverter.canRead(String.class, contentType)).andReturn(true); + expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(expected); + + replay(messageConverter); + + Object result = processor.resolveArgument(stringParameter, null, webRequest, null); + assertEquals("Invalid argument", expected, result); + + verify(messageConverter); + + } + + @Test(expected = HttpMediaTypeNotSupportedException.class) + public void resolveArgumentNotReadable() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + + servletRequest.addHeader("Content-Type", contentType.toString()); + + expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType)); + expect(messageConverter.canRead(String.class, contentType)).andReturn(false); + + replay(messageConverter); + + processor.resolveArgument(stringParameter, null, webRequest, null); + + verify(messageConverter); + } + + @Test(expected = HttpMediaTypeNotSupportedException.class) + public void resolveArgumentNoContentType() throws Exception { + processor.resolveArgument(stringParameter, null, webRequest, null); + } + + @Test + public void handleReturnValue() throws Exception { + MediaType accepted = MediaType.TEXT_PLAIN; + String returnValue = "Foo"; + + servletRequest.addHeader("Accept", accepted.toString()); + + expect(messageConverter.canWrite(String.class, accepted)).andReturn(true); + messageConverter.write(eq(returnValue), eq(accepted), isA(HttpOutputMessage.class)); + + replay(messageConverter); + + processor.handleReturnValue(returnValue, stringReturnValue, null, webRequest); + + verify(messageConverter); + } + + @Test(expected = HttpMediaTypeNotAcceptableException.class) + public void handleReturnValueNotAcceptable() throws Exception { + MediaType accepted = MediaType.TEXT_PLAIN; + String returnValue = "Foo"; + + servletRequest.addHeader("Accept", accepted.toString()); + + expect(messageConverter.canWrite(String.class, accepted)).andReturn(false); + expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM)); + + replay(messageConverter); + + processor.handleReturnValue(returnValue, stringReturnValue, null, webRequest); + + verify(messageConverter); + } + + @ResponseBody + public String handle(@RequestBody String s, int i) { + return s; + } + + public int otherMethod() { + return 42; + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolverTests.java new file mode 100644 index 0000000000..f9badf04c3 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletCookieValueMethodArgumentResolverTests.java @@ -0,0 +1,117 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import javax.servlet.http.Cookie; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.CookieValueMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletCookieValueMethodArgumentResolver; + +/** + * @author Arjen Poutsma + */ +public class ServletCookieValueMethodArgumentResolverTests { + + private CookieValueMethodArgumentResolver resolver; + + private MethodParameter cookieParameter; + + private MethodParameter cookieStringParameter; + + private MethodParameter otherParameter; + + private MockHttpServletRequest servletRequest; + + private ServletWebRequest webRequest; + + @Before + public void setUp() throws Exception { + resolver = new ServletCookieValueMethodArgumentResolver(null); + Method method = getClass().getMethod("params", Cookie.class, String.class, String.class); + cookieParameter = new MethodParameter(method, 0); + cookieStringParameter = new MethodParameter(method, 1); + otherParameter = new MethodParameter(method, 2); + + servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); + + } + + @Test + public void usesResponseArgument() throws NoSuchMethodException { + assertFalse("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue("Cookie parameter not supported", resolver.supportsParameter(cookieParameter)); + assertTrue("Cookie string parameter not supported", resolver.supportsParameter(cookieStringParameter)); + assertFalse("non-@CookieValue parameter supported", resolver.supportsParameter(otherParameter)); + } + + @Test + public void resolveCookieArgument() throws Exception { + Cookie expected = new Cookie("name", "foo"); + servletRequest.setCookies(expected); + + Cookie result = (Cookie) resolver.resolveArgument(cookieParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveCookieStringArgument() throws Exception { + Cookie cookie = new Cookie("name", "foo"); + servletRequest.setCookies(cookie); + + String result = (String) resolver.resolveArgument(cookieStringParameter, null, webRequest, null); + assertEquals("Invalid result", cookie.getValue(), result); + } + + @Test + public void resolveCookieDefaultValue() throws Exception { + String result = (String) resolver.resolveArgument(cookieStringParameter, null, webRequest, null); + assertEquals("Invalid result", "bar", result); + } + + @Test(expected = IllegalStateException.class) + public void notFound() throws Exception { + String result = (String) resolver.resolveArgument(cookieParameter, null, webRequest, null); + assertEquals("Invalid result", "bar", result); + } + + public void params(@CookieValue("name") Cookie cookie, + @CookieValue(value = "name", defaultValue = "bar") String cookieString, + String unsupported) { + + } + + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolverTests.java new file mode 100644 index 0000000000..0360a5c922 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletRequestMethodArgumentResolverTests.java @@ -0,0 +1,147 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Method; +import java.security.Principal; +import java.util.Locale; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpSession; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.multipart.MultipartRequest; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class ServletRequestMethodArgumentResolverTests { + + private ServletRequestMethodArgumentResolver resolver; + + private Method supportedParams; + + private ServletWebRequest webRequest; + + private MockHttpServletRequest servletRequest; + + @Before + public void setUp() throws Exception { + resolver = new ServletRequestMethodArgumentResolver(); + supportedParams = getClass() + .getMethod("supportedParams", ServletRequest.class, MultipartRequest.class, HttpSession.class, + Principal.class, Locale.class, InputStream.class, Reader.class); + servletRequest = new MockHttpServletRequest(); + webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); + } + + @Test + public void usesResponseArgument() { + assertFalse("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void servletRequest() throws Exception { + MethodParameter servletRequestParameter = new MethodParameter(supportedParams, 0); + + assertTrue("ServletRequest not supported", resolver.supportsParameter(servletRequestParameter)); + + Object result = resolver.resolveArgument(servletRequestParameter, null, webRequest, null); + assertSame("Invalid result", servletRequest, result); + } + + @Test + public void session() throws Exception { + MockHttpSession session = new MockHttpSession(); + servletRequest.setSession(session); + MethodParameter sessionParameter = new MethodParameter(supportedParams, 2); + + assertTrue("Session not supported", resolver.supportsParameter(sessionParameter)); + + Object result = resolver.resolveArgument(sessionParameter, null, webRequest, null); + assertSame("Invalid result", session, result); + } + + @Test + public void principal() throws Exception { + Principal principal = new Principal() { + public String getName() { + return "Foo"; + } + }; + servletRequest.setUserPrincipal(principal); + MethodParameter principalParameter = new MethodParameter(supportedParams, 3); + + assertTrue("Principal not supported", resolver.supportsParameter(principalParameter)); + + Object result = resolver.resolveArgument(principalParameter, null, webRequest, null); + assertSame("Invalid result", principal, result); + } + + @Test + public void locale() throws Exception { + Locale locale = Locale.ENGLISH; + servletRequest.addPreferredLocale(locale); + MethodParameter localeParameter = new MethodParameter(supportedParams, 4); + + assertTrue("Locale not supported", resolver.supportsParameter(localeParameter)); + + Object result = resolver.resolveArgument(localeParameter, null, webRequest, null); + assertSame("Invalid result", locale, result); + } + + @Test + public void inputStream() throws Exception { + MethodParameter inputStreamParameter = new MethodParameter(supportedParams, 5); + + assertTrue("InputStream not supported", resolver.supportsParameter(inputStreamParameter)); + + Object result = resolver.resolveArgument(inputStreamParameter, null, webRequest, null); + assertSame("Invalid result", webRequest.getRequest().getInputStream(), result); + } + + @Test + public void reader() throws Exception { + MethodParameter readerParameter = new MethodParameter(supportedParams, 6); + + assertTrue("Reader not supported", resolver.supportsParameter(readerParameter)); + + Object result = resolver.resolveArgument(readerParameter, null, webRequest, null); + assertSame("Invalid result", webRequest.getRequest().getReader(), result); + } + + public void supportedParams(ServletRequest p0, + MultipartRequest p1, + HttpSession p2, + Principal p3, + Locale p4, + InputStream p5, + Reader p9) { + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolverTests.java new file mode 100644 index 0000000000..4cccd05422 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletResponseMethodArgumentResolverTests.java @@ -0,0 +1,95 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import java.io.OutputStream; +import java.io.Writer; +import java.lang.reflect.Method; +import javax.servlet.ServletResponse; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class ServletResponseMethodArgumentResolverTests { + + private ServletResponseMethodArgumentResolver resolver; + + private Method supportedParams; + + private ServletWebRequest webRequest; + + private MockHttpServletResponse servletResponse; + + @Before + public void setUp() throws Exception { + resolver = new ServletResponseMethodArgumentResolver(); + supportedParams = + getClass().getMethod("supportedParams", ServletResponse.class, OutputStream.class, Writer.class); + servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(new MockHttpServletRequest(), servletResponse); + } + + @Test + public void usesResponseArgument() { + assertTrue("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void servletResponse() throws Exception { + MethodParameter servletResponseParameter = new MethodParameter(supportedParams, 0); + + assertTrue("ServletResponse not supported", resolver.supportsParameter(servletResponseParameter)); + + Object result = resolver.resolveArgument(servletResponseParameter, null, webRequest, null); + assertSame("Invalid result", servletResponse, result); + } + + @Test + public void outputStream() throws Exception { + MethodParameter outputStreamParameter = new MethodParameter(supportedParams, 1); + + assertTrue("OutputStream not supported", resolver.supportsParameter(outputStreamParameter)); + + Object result = resolver.resolveArgument(outputStreamParameter, null, webRequest, null); + assertSame("Invalid result", servletResponse.getOutputStream(), result); + } + + @Test + public void writer() throws Exception { + MethodParameter writerParameter = new MethodParameter(supportedParams, 2); + + assertTrue("Writer not supported", resolver.supportsParameter(writerParameter)); + + Object result = resolver.resolveArgument(writerParameter, null, webRequest, null); + assertSame("Invalid result", servletResponse.getWriter(), result); + } + + public void supportedParams(ServletResponse p0, OutputStream p1, Writer p2) { + + } +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java new file mode 100644 index 0000000000..bc7d03fda0 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java @@ -0,0 +1,91 @@ +/* + * 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.servlet.mvc.method.annotation.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; +import org.springframework.web.servlet.view.InternalResourceView; + +/** + * Test fixture for {@link DefaultMethodReturnValueHandler} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ViewMethodReturnValueHandlerTests { + + private ViewMethodReturnValueHandler handler; + + private ServletWebRequest webRequest; + + private ModelAndViewContainer mavContainer; + + @Before + public void setUp() { + this.handler = new ViewMethodReturnValueHandler(); + this.mavContainer = new ModelAndViewContainer(new ExtendedModelMap()); + this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + } + + @Test + public void supportsReturnType() throws Exception { + assertTrue(handler.supportsReturnType(createMethodParam("view"))); + assertTrue(handler.supportsReturnType(createMethodParam("viewName"))); + } + + @Test + public void returnView() throws Exception { + InternalResourceView view = new InternalResourceView("testView"); + handler.handleReturnValue(view, createMethodParam("view"), mavContainer, webRequest); + assertSame(view, mavContainer.getView()); + } + + @Test + public void returnViewName() throws Exception { + handler.handleReturnValue("testView", createMethodParam("viewName"), mavContainer, webRequest); + assertEquals("testView", mavContainer.getViewName()); + } + + private MethodParameter createMethodParam(String methodName) throws Exception { + Method method = getClass().getDeclaredMethod(methodName); + return new MethodParameter(method, -1); + } + + @SuppressWarnings("unused") + private View view() { + return null; + } + + @SuppressWarnings("unused") + private String viewName() { + return null; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java new file mode 100644 index 0000000000..8613001f06 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java @@ -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); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebDataBinderFactory.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebDataBinderFactory.java new file mode 100644 index 0000000000..ba72a92a3a --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebDataBinderFactory.java @@ -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; + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethod.java b/org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethod.java new file mode 100644 index 0000000000..552b715dce --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -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. + * + *

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 getMethodAnnotation(Class 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 getMethodAnnotation(Class annotationType) { + return HandlerMethod.this.getMethodAnnotation(annotationType); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java b/org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java new file mode 100644 index 0000000000..4db566080c --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java @@ -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 selectMethods(final Class handlerType, final MethodFilter handlerMethodFilter) { + final Set handlerMethods = new LinkedHashSet(); + Set> handlerTypes = new LinkedHashSet>(); + 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; + } + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java new file mode 100644 index 0000000000..2ab1f796da --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java @@ -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. + * + *

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. + * + *

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, Method> mappedExceptionTypes = + new HashMap, Method>(); + + private final Map, Method> resolvedExceptionTypes = + new ConcurrentHashMap, Method>(); + + /** + * Creates an {@link ExceptionMethodMapping} instance from a set of {@link ExceptionHandler} methods. + *

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 methods) { + initExceptionMap(methods); + } + + /** + * Examines the provided methods and populates mapped exception types. + */ + private void initExceptionMap(Set methods) { + for (Method method : methods) { + for (Class 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: + *

    + *
  1. The {@link ExceptionHandler} annotation value + *
  2. {@link Throwable} types that appear in the method parameter list + *
+ * @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> getMappedExceptionTypes(Method method) { + ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); + if (annotation.value().length != 0) { + return Arrays.asList(annotation.value()); + } + else { + List> result = new ArrayList>(); + for (Class paramType : method.getParameterTypes()) { + if (Throwable.class.isAssignableFrom(paramType)) { + result.add((Class) 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 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 exceptionType) { + List> matches = new ArrayList>(); + for(Class 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 getBestMatchingExceptionType(List> exceptionTypes, + Class 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); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactory.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactory.java new file mode 100644 index 0000000000..da262aa98c --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactory.java @@ -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 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 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 attributeNames = new HashSet(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; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java new file mode 100644 index 0000000000..8f3c3ea95d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java @@ -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. + * + *

{@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. + * + *

{@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 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 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: + *

    + *
  1. Retrieve previously accessed handler session attributes from the session + *
  2. Invoke model attribute methods + *
  3. Find request-handling method {@link ModelAttribute}-annotated arguments that are handler session attributes + *
+ *

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 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: + *

    + *
  1. The method {@link ModelAttribute} annotation value + *
  2. The name of the return type + *
  3. The name of the return value type if the method return type is {@code Object} + *
+ * @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: + *
    + *
  1. The parameter {@link ModelAttribute} annotation value + *
  2. The name of the parameter type + *
+ * @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 keyNames = new ArrayList(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())); + } + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java new file mode 100644 index 0000000000..c51cb0ea86 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java @@ -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}. + * + *

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. + * + *

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 attributeNames = new HashSet(); + + @SuppressWarnings("rawtypes") + private final Set attributeTypes = new HashSet(); + + private final Set resolvedAttributeNames = Collections.synchronizedSet(new HashSet(4)); + + private final SessionAttributeStore attributeStore; + + /** + * Creates a {@link SessionAttributesHandler} instance for the specified handlerType. + *

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. + *

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. + *

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 retrieveHandlerSessionAttributes(WebRequest request) { + Map attributes = new HashMap(); + 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. + *

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. + *

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 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); + } + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/AbstractNamedValueMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/AbstractNamedValueMethodArgumentResolver.java new file mode 100644 index 0000000000..df6cb17ca7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/AbstractNamedValueMethodArgumentResolver.java @@ -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 namedValueInfoCache = + new ConcurrentHashMap(); + + 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. + * + *

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. + * + *

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; + } + + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/CookieValueMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/CookieValueMethodArgumentResolver.java new file mode 100644 index 0000000000..8cdd58d2a8 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/CookieValueMethodArgumentResolver.java @@ -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()); + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ErrorsMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ErrorsMethodArgumentResolver.java new file mode 100644 index 0000000000..36caddad40 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ErrorsMethodArgumentResolver.java @@ -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 keys = new ArrayList(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); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java new file mode 100644 index 0000000000..2602e78568 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java @@ -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()); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java new file mode 100644 index 0000000000..4ea93dbdf7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java @@ -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 void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + String name = ModelFactory.getNameForReturnValue(returnValue, returnType); + mavContainer.addModelAttribute(name, returnValue); + } + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelMethodProcessor.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelMethodProcessor.java new file mode 100644 index 0000000000..aa33686d6d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelMethodProcessor.java @@ -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 void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + if (returnValue instanceof Model) { + mavContainer.addModelAttributes((Model) returnValue); + } + else { + mavContainer.addModelAttributes((Map) returnValue); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolver.java new file mode 100644 index 0000000000..cc249fb475 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolver.java @@ -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 result; + if (HttpHeaders.class.isAssignableFrom(paramType)) { + result = new HttpHeaders(); + } + else { + result = new LinkedMultiValueMap(); + } + for (Iterator iterator = webRequest.getHeaderNames(); iterator.hasNext();) { + String headerName = iterator.next(); + for (String headerValue : webRequest.getHeaderValues(headerName)) { + result.add(headerName, headerValue); + } + } + return result; + } + else { + Map result = new LinkedHashMap(); + for (Iterator iterator = webRequest.getHeaderNames(); iterator.hasNext();) { + String headerName = iterator.next(); + String headerValue = webRequest.getHeader(headerName); + result.put(headerName, headerValue); + } + return result; + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolver.java new file mode 100644 index 0000000000..1ff704499b --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolver.java @@ -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()); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolver.java new file mode 100644 index 0000000000..d2fd873d43 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolver.java @@ -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 parameterMap = webRequest.getParameterMap(); + if (MultiValueMap.class.isAssignableFrom(paramType)) { + MultiValueMap result = new LinkedMultiValueMap(parameterMap.size()); + for (Map.Entry entry : parameterMap.entrySet()) { + for (String value : entry.getValue()) { + result.add(entry.getKey(), value); + } + } + return result; + } + else { + Map result = new LinkedHashMap(parameterMap.size()); + for (Map.Entry entry : parameterMap.entrySet()) { + if (entry.getValue().length > 0) { + result.put(entry.getKey(), entry.getValue()[0]); + } + } + return result; + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java new file mode 100644 index 0000000000..41889d0c60 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java @@ -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 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()); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapter.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapter.java new file mode 100644 index 0000000000..c956e1ea44 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapter.java @@ -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; + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..86fa88e0c1 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolver.java @@ -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; + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainer.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainer.java new file mode 100644 index 0000000000..cf5e74d84b --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainer.java @@ -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. + * + *

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 argumentResolvers = + new ArrayList(); + + private Map argumentResolverCache = + new ConcurrentHashMap(); + + /** + * 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); + } + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodProcessor.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodProcessor.java new file mode 100644 index 0000000000..48cb96614a --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodProcessor.java @@ -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); + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandler.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandler.java new file mode 100644 index 0000000000..f41c0c8a54 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandler.java @@ -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 + */ + void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception; + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainer.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainer.java new file mode 100644 index 0000000000..3a4b53626e --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainer.java @@ -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. + * + *

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 returnValueHandlers = + new ArrayList(); + + private Map returnValueHandlerCache = + new ConcurrentHashMap(); + + /** + * 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 void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer 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); + } + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java new file mode 100644 index 0000000000..4f1be95ba9 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -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. + * + *

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). + *

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. + *

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; + } + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java new file mode 100644 index 0000000000..c036f5f3f0 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java @@ -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. + * + *

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 Servlet or Portlet specific View type. + */ +public class ModelAndViewContainer { + + 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 attributes) { + actualModel.addAllAttributes(attributes); + } + + public void addModelAttribute(String name, Object value) { + actualModel.addAttribute(name, value); + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/HandlerSessionAttributeStoreTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/HandlerSessionAttributeStoreTests.java new file mode 100644 index 0000000000..d7f953c276 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/HandlerSessionAttributeStoreTests.java @@ -0,0 +1,126 @@ +/* + * 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 static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.TestBean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.support.DefaultSessionAttributeStore; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.SessionAttributesHandler; + +/** + * Test fixture for {@link SessionAttributesHandler} unit tests. + * + * @author Rossen Stoyanchev + */ +public class HandlerSessionAttributeStoreTests { + + private DefaultSessionAttributeStore sessionAttributes; + + private SessionAttributesHandler handlerSessionAttributes; + + private Class handlerType = SessionAttributeHandler.class; + + private NativeWebRequest request; + + @Before + public void setUp() { + this.sessionAttributes = new DefaultSessionAttributeStore(); + this.handlerSessionAttributes = new SessionAttributesHandler(handlerType, sessionAttributes); + + this.request = new ServletWebRequest(new MockHttpServletRequest()); + } + + @Test + public void isSessionAttribute() throws Exception { + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr1", null)); + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr2", null)); + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("simple", TestBean.class)); + + assertFalse("Attribute name not known", handlerSessionAttributes.isHandlerSessionAttribute("simple", null)); + } + + @Test + public void retrieveAttributes() throws Exception { + sessionAttributes.storeAttribute(request, "attr1", "value1"); + sessionAttributes.storeAttribute(request, "attr2", "value2"); + sessionAttributes.storeAttribute(request, "attr3", new TestBean()); + + // Query attributes to associate them with the handler type + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr1", null)); + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr3", TestBean.class)); + + Map attributes = handlerSessionAttributes.retrieveHandlerSessionAttributes(request); + + assertEquals(new HashSet(asList("attr1", "attr3")), attributes.keySet()); + } + + @Test + public void cleanupAttribute() throws Exception { + sessionAttributes.storeAttribute(request, "attr1", "value1"); + sessionAttributes.storeAttribute(request, "attr2", "value2"); + sessionAttributes.storeAttribute(request, "attr3", new TestBean()); + + // Query attribute to associate it with the handler type + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr1", null)); + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr3", TestBean.class)); + + handlerSessionAttributes.cleanupHandlerSessionAttributes(request); + + assertNull(sessionAttributes.retrieveAttribute(request, "attr1")); + assertNotNull(sessionAttributes.retrieveAttribute(request, "attr2")); + assertNull(sessionAttributes.retrieveAttribute(request, "attr3")); + } + + @Test + public void storeAttributes() throws Exception { + Map attributes = new HashMap(); + attributes.put("attr1", "value1"); + attributes.put("attr2", "value2"); + attributes.put("attr3", new TestBean()); + + // Query attribute to associate it with the handler type + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr1", null)); + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr2", null)); + assertTrue(handlerSessionAttributes.isHandlerSessionAttribute("attr3", TestBean.class)); + + handlerSessionAttributes.storeHandlerSessionAttributes(request, attributes); + + assertEquals("value1", sessionAttributes.retrieveAttribute(request, "attr1")); + assertEquals("value2", sessionAttributes.retrieveAttribute(request, "attr2")); + assertTrue(sessionAttributes.retrieveAttribute(request, "attr3") instanceof TestBean); + } + + @SessionAttributes(value = { "attr1", "attr2" }, types = { TestBean.class }) + private static class SessionAttributeHandler { + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactoryTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactoryTests.java new file mode 100644 index 0000000000..aabcfceda3 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderMethodDataBinderFactoryTests.java @@ -0,0 +1,168 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.DefaultDataBinderFactory; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.RequestParamMethodArgumentResolver; +import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.method.support.HandlerMethodArgumentResolverContainer; + +/** + * Test fixture for {@link InitBinderMethodDataBinderFactory} unit tests. + * + * @author Rossen Stoyanchev + */ +public class InitBinderMethodDataBinderFactoryTests { + + private MockHttpServletRequest request; + + private NativeWebRequest webRequest; + + private ConfigurableWebBindingInitializer bindingInitializer; + + @Before + public void setUp() throws Exception { + this.request = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(request); + this.bindingInitializer = new ConfigurableWebBindingInitializer(); + } + + @Test + public void createBinder() throws Exception { + InitBinderMethodDataBinderFactory factory = createFactory("initBinder", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); + + assertNotNull(dataBinder.getDisallowedFields()); + assertEquals("id", dataBinder.getDisallowedFields()[0]); + } + + @Test + public void createBinderWithGlobalInitialization() throws Exception { + ConversionService conversionService = new DefaultFormattingConversionService(); + bindingInitializer.setConversionService(conversionService ); + + InitBinderMethodDataBinderFactory factory = createFactory("initBinder", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); + + assertSame(conversionService, dataBinder.getConversionService()); + } + + @Test + public void createBinderWithAttrName() throws Exception { + InitBinderMethodDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(webRequest, null, "foo"); + + assertNotNull(dataBinder.getDisallowedFields()); + assertEquals("id", dataBinder.getDisallowedFields()[0]); + } + + @Test + public void createBinderWithAttrNameNoMatch() throws Exception { + WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(webRequest, null, "invalidName"); + + assertNull(dataBinder.getDisallowedFields()); + } + + @Test(expected=IllegalStateException.class) + public void returnValueNotExpected() throws Exception { + WebDataBinderFactory factory = createFactory("initBinderReturnValue", WebDataBinder.class); + factory.createBinder(webRequest, null, "invalidName"); + } + + @Test + public void createBinderTypeConversion() throws Exception { + request.setParameter("requestParam", "22"); + + HandlerMethodArgumentResolverContainer argResolvers = new HandlerMethodArgumentResolverContainer(); + argResolvers.registerArgumentResolver(new RequestParamMethodArgumentResolver(null, false)); + + String methodName = "initBinderTypeConversion"; + WebDataBinderFactory factory = createFactory(argResolvers, methodName, WebDataBinder.class, int.class); + WebDataBinder dataBinder = factory.createBinder(webRequest, null, "foo"); + + assertNotNull(dataBinder.getDisallowedFields()); + assertEquals("requestParam-22", dataBinder.getDisallowedFields()[0]); + } + + private InitBinderMethodDataBinderFactory createFactory(String methodName, Class... parameterTypes) + throws Exception { + return createFactory(new HandlerMethodArgumentResolverContainer(), methodName, parameterTypes); + } + + private InitBinderMethodDataBinderFactory createFactory(HandlerMethodArgumentResolverContainer argResolvers, + String methodName, Class... parameterTypes) throws Exception { + Object handler = new InitBinderHandler(); + Method method = InitBinderHandler.class.getMethod(methodName, parameterTypes); + + InvocableHandlerMethod controllerMethod = new InvocableHandlerMethod(handler, method); + controllerMethod.setArgumentResolverContainer(argResolvers); + controllerMethod.setDataBinderFactory(new DefaultDataBinderFactory(null)); + controllerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + + return new InitBinderMethodDataBinderFactory(Arrays.asList(controllerMethod), bindingInitializer); + } + + private static class InitBinderHandler { + + @SuppressWarnings("unused") + @InitBinder + public void initBinder(WebDataBinder dataBinder) { + dataBinder.setDisallowedFields("id"); + } + + @SuppressWarnings("unused") + @InitBinder(value="foo") + public void initBinderWithAttributeName(WebDataBinder dataBinder) { + dataBinder.setDisallowedFields("id"); + } + + @SuppressWarnings("unused") + @InitBinder + public String initBinderReturnValue(WebDataBinder dataBinder) { + return "invalid"; + } + + @SuppressWarnings("unused") + @InitBinder + public void initBinderTypeConversion(WebDataBinder dataBinder, @RequestParam int requestParam) { + dataBinder.setDisallowedFields("requestParam-" + requestParam); + } + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java new file mode 100644 index 0000000000..afb242a0a0 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java @@ -0,0 +1,199 @@ +/* + * 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 static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.support.DefaultSessionAttributeStore; +import org.springframework.web.bind.support.SessionAttributeStore; +import org.springframework.web.bind.support.SimpleSessionStatus; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.ModelMethodProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolverContainer; +import org.springframework.web.method.support.InvocableHandlerMethod; + +/** + * Text fixture for {@link ModelFactory} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ModelFactoryTests { + + private NativeWebRequest webRequest; + + private SessionAttributeStore sessionAttributeStore; + + private SessionAttributesHandler handlerSessionAttributeStore; + + private InvocableHandlerMethod requestMethod; + + @Before + public void setUp() throws Exception { + Object handler = new ModelHandler(); + Method method = handler.getClass().getDeclaredMethod("handle"); + this.requestMethod = new InvocableHandlerMethod(handler, method); + + this.sessionAttributeStore = new DefaultSessionAttributeStore(); + this.handlerSessionAttributeStore = new SessionAttributesHandler(handler.getClass(), sessionAttributeStore); + + this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + } + + @Test + public void createModel() throws Exception { + ModelMap model = createFactory(new ModelHandler(), "model", Model.class).createModel(webRequest, requestMethod); + assertEquals(Boolean.TRUE, model.get("model")); + } + + @Test + public void createModelWithName() throws Exception { + ModelMap model = createFactory(new ModelHandler(), "modelWithName").createModel(webRequest, requestMethod); + assertEquals(Boolean.TRUE, model.get("name")); + } + + @Test + public void createModelWithDefaultName() throws Exception { + ModelMap model = createFactory(new ModelHandler(), "modelWithDefaultName").createModel(webRequest, requestMethod); + assertEquals(Boolean.TRUE, model.get("boolean")); + } + + @Test + public void createModelWithExistingName() throws Exception { + ModelMap model = createFactory(new ModelHandler(), "modelWithName").createModel(webRequest, requestMethod); + assertEquals(Boolean.TRUE, model.get("name")); + } + + @Test + public void createModelWithNullAttribute() throws Exception { + ModelMap model = createFactory(new ModelHandler(), "modelWithNullAttribute").createModel(webRequest, requestMethod); + assertTrue(model.containsKey("name")); + assertNull(model.get("name")); + } + + @Test + public void createModelExistingSessionAttributes() throws Exception { + sessionAttributeStore.storeAttribute(webRequest, "sessionAttr", "sessionAttrValue"); + + // Query attribute to associate it with the handler type + assertTrue(handlerSessionAttributeStore.isHandlerSessionAttribute("sessionAttr", null)); + + ModelMap model = createFactory(new ModelHandler(), "model", Model.class).createModel(webRequest, requestMethod); + assertEquals("sessionAttrValue", model.get("sessionAttr")); + } + + @Test + public void updateBindingResult() throws Exception { + Object handler = new ModelHandler(); + + SessionAttributeStore store = new DefaultSessionAttributeStore(); + SessionAttributesHandler sessionAttributeStore = new SessionAttributesHandler(handler.getClass(), store); + + String attrName = "attr1"; + Object attrValue = new Object(); + + ModelMap actualModel = new ExtendedModelMap(); + actualModel.addAttribute(attrName, attrValue); + + WebDataBinder dataBinder = new WebDataBinder(attrValue, attrName); + + WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class); + expect(binderFactory.createBinder(webRequest, attrValue, attrName)).andReturn(dataBinder); + replay(binderFactory); + + ModelFactory modelFactory = new ModelFactory(null, binderFactory, sessionAttributeStore); + modelFactory.updateAttributes(webRequest, new SimpleSessionStatus(), actualModel, null); + + assertEquals(attrValue, actualModel.remove(attrName)); + assertSame(dataBinder.getBindingResult(), actualModel.remove(bindingResultKey(attrName))); + assertEquals(0, actualModel.size()); + + verify(binderFactory); + } + + private String bindingResultKey(String key) { + return BindingResult.MODEL_KEY_PREFIX + key; + } + + private ModelFactory createFactory(Object handler, String methodName, Class... parameterTypes) throws Exception{ + Method method = ModelHandler.class.getMethod(methodName, parameterTypes); + + HandlerMethodArgumentResolverContainer argResolvers = new HandlerMethodArgumentResolverContainer(); + argResolvers.registerArgumentResolver(new ModelMethodProcessor()); + + InvocableHandlerMethod controllerMethod = new InvocableHandlerMethod(handler, method); + controllerMethod.setArgumentResolverContainer(argResolvers); + controllerMethod.setDataBinderFactory(null); + controllerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + + return new ModelFactory(Arrays.asList(controllerMethod), null, handlerSessionAttributeStore); + } + + @SessionAttributes("sessionAttr") + private static class ModelHandler { + + @SuppressWarnings("unused") + @ModelAttribute + public void model(Model model) { + model.addAttribute("model", Boolean.TRUE); + } + + @SuppressWarnings("unused") + @ModelAttribute("name") + public Boolean modelWithName() { + return Boolean.TRUE; + } + + @SuppressWarnings("unused") + @ModelAttribute + public Boolean modelWithDefaultName() { + return Boolean.TRUE; + } + + @SuppressWarnings("unused") + @ModelAttribute("name") + public Boolean modelWithNullAttribute() { + return null; + } + + @SuppressWarnings("unused") + public void handle() { + } + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ErrorsMethodHandlerArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ErrorsMethodHandlerArgumentResolverTests.java new file mode 100644 index 0000000000..df864c1ccc --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ErrorsMethodHandlerArgumentResolverTests.java @@ -0,0 +1,125 @@ +/* + * 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 static org.junit.Assert.assertSame; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.ErrorsMethodArgumentResolver; + +/** + * Test fixture for {@link ErrorsMethodArgumentResolver} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ErrorsMethodHandlerArgumentResolverTests { + + private NativeWebRequest webRequest; + + private ErrorsMethodArgumentResolver resolver; + + private MethodParameter errorsParam; + + @Before + public void setUp() throws Exception { + this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + + this.resolver = new ErrorsMethodArgumentResolver(); + + Method method = this.getClass().getDeclaredMethod("handle", Errors.class); + this.errorsParam = new MethodParameter(method, 0); + } + + @Test + public void supports() throws Exception { + resolver.supportsParameter(errorsParam); + } + + @Test + public void bindingResult() throws Exception { + WebDataBinder dataBinder = new WebDataBinder(new Object(), "attr"); + BindingResult bindingResult = dataBinder.getBindingResult(); + + ModelMap model = new ExtendedModelMap(); + model.addAttribute("ignore1", "value1"); + model.addAttribute("ignore2", "value2"); + model.addAttribute("ignore3", "value3"); + model.addAttribute("ignore4", "value4"); + model.addAttribute("ignore5", "value5"); + model.addAllAttributes(bindingResult.getModel()); // Predictable iteration order of model keys important! + + Object actual = resolver.resolveArgument(errorsParam, model, webRequest, null); + + assertSame(actual, bindingResult); + } + + @Test + public void bindingResultExistingModelAttribute() throws Exception { + Object target1 = new Object(); + WebDataBinder binder1 = new WebDataBinder(target1, "attr1"); + BindingResult bindingResult1 = binder1.getBindingResult(); + + Object target2 = new Object(); + WebDataBinder binder2 = new WebDataBinder(target1, "attr2"); + BindingResult bindingResult2 = binder2.getBindingResult(); + + ModelMap model = new ExtendedModelMap(); + model.addAttribute("attr1", target1); + model.addAttribute("attr2", target2); + model.addAttribute("filler", "fillerValue"); + model.addAllAttributes(bindingResult1.getModel()); + model.addAllAttributes(bindingResult2.getModel()); + + Object actual = resolver.resolveArgument(errorsParam, model, webRequest, null); + + assertSame("Should resolve to the latest BindingResult added", actual, bindingResult2); + } + + @Test(expected=IllegalStateException.class) + public void bindingResultNotFound() throws Exception { + WebDataBinder dataBinder = new WebDataBinder(new Object(), "attr"); + BindingResult bindingResult = dataBinder.getBindingResult(); + + ModelMap model = new ExtendedModelMap(); + model.addAllAttributes(bindingResult.getModel()); + model.addAttribute("ignore1", "value1"); + + resolver.resolveArgument(errorsParam, model, webRequest, null); + } + + @Test(expected=IllegalStateException.class) + public void noBindingResult() throws Exception { + resolver.resolveArgument(errorsParam, new ExtendedModelMap(), webRequest, null); + } + + @SuppressWarnings("unused") + private void handle(Errors errors) { + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolverTests.java new file mode 100644 index 0000000000..256a09d823 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolverTests.java @@ -0,0 +1,110 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.method.annotation.support.ExpressionValueMethodArgumentResolver; + +/** + * Test fixture for {@link ExpressionValueMethodArgumentResolver} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ExpressionValueMethodArgumentResolverTests { + + private ExpressionValueMethodArgumentResolver resolver; + + private MethodParameter systemParameter; + + private MethodParameter requestParameter; + + private MethodParameter unsupported; + + private MockHttpServletRequest servletRequest; + + private NativeWebRequest webRequest; + + @Before + public void setUp() throws Exception { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.refresh(); + + resolver = new ExpressionValueMethodArgumentResolver(context.getBeanFactory()); + + Method method = getClass().getMethod("params", int.class, String.class, String.class); + systemParameter = new MethodParameter(method, 0); + requestParameter = new MethodParameter(method, 1); + unsupported = new MethodParameter(method, 2); + + servletRequest = new MockHttpServletRequest(); + webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); + + // Expose request to the current thread (for SpEL expressions) + RequestContextHolder.setRequestAttributes(webRequest); + } + + @After + public void teardown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void supportsParameter() throws Exception { + assertTrue(resolver.supportsParameter(systemParameter)); + assertTrue(resolver.supportsParameter(requestParameter)); + + assertFalse(resolver.supportsParameter(unsupported)); + } + + @Test + public void resolveSystemProperty() throws Exception { + System.setProperty("systemIntValue", "22"); + Object value = resolver.resolveArgument(systemParameter, null, webRequest, null); + + assertEquals("22", value); + } + + @Test + public void resolveRequestProperty() throws Exception { + servletRequest.setContextPath("/contextPath"); + Object value = resolver.resolveArgument(requestParameter, null, webRequest, null); + + assertEquals("/contextPath", value); + } + + public void params(@Value("#{systemProperties.systemIntValue}") int param1, + @Value("#{request.contextPath}") String param2, + String unsupported) { + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java new file mode 100644 index 0000000000..694fa72767 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessorTests.java @@ -0,0 +1,275 @@ +/* + * 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 static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.notNull; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.TestBean; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.SessionAttributes; +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.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Test fixture for {@link ModelAttributeMethodProcessor} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ModelAttributeMethodProcessorTests { + + private ModelAttributeMethodProcessor processor; + + private MethodParameter annotatedParam; + + private MethodParameter integerParam; + + private MethodParameter defaultNameParam; + + private MethodParameter notAnnotatedParam; + + private MethodParameter annotatedReturnParam; + + private MethodParameter notAnnotatedReturnParam; + + private ModelMap model; + + private NativeWebRequest webRequest; + + @Before + public void setUp() throws Exception { + this.processor = new ModelAttributeMethodProcessor(true); + + Class handlerType = ModelAttributeHandler.class; + Method method = handlerType.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, + TestBean.class, TestBean.class); + + this.annotatedParam = new MethodParameter(method, 0); + this.integerParam = new MethodParameter(method, 2); + this.defaultNameParam = new MethodParameter(method, 3); + this.notAnnotatedParam = new MethodParameter(method, 4); + + this.annotatedReturnParam = new MethodParameter(getClass().getDeclaredMethod("annotatedReturnValue"), -1); + this.notAnnotatedReturnParam = new MethodParameter(getClass().getDeclaredMethod("notAnnotatedReturnValue"), -1); + + model = new ExtendedModelMap(); + + this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + } + + @Test + public void supportParameter() throws Exception { + assertTrue(processor.supportsParameter(annotatedParam)); + assertFalse(processor.supportsParameter(integerParam)); + assertTrue(processor.supportsParameter(notAnnotatedParam)); + + this.processor = new ModelAttributeMethodProcessor(false); + assertFalse(processor.supportsParameter(notAnnotatedParam)); + } + + @Test + public void supportsReturnType() throws Exception { + assertTrue(processor.supportsReturnType(annotatedReturnParam)); + assertFalse(processor.supportsReturnType(notAnnotatedReturnParam)); + } + + @Test + public void shouldValidate() throws Exception { + assertTrue(processor.shouldValidate(annotatedParam)); + assertFalse(processor.shouldValidate(notAnnotatedParam)); + } + + @Test + public void failOnError() throws Exception { + assertFalse(processor.failOnError(annotatedParam)); + assertTrue(processor.failOnError(notAnnotatedParam)); + } + + @Test + public void createBinderFromModelAttribute() throws Exception { + createBinderFromModelAttr("attrName", annotatedParam); + createBinderFromModelAttr("testBean", defaultNameParam); + createBinderFromModelAttr("testBean", notAnnotatedParam); + } + + private void createBinderFromModelAttr(String expectedAttrName, MethodParameter param) throws Exception { + Object target = new TestBean(); + model.addAttribute(expectedAttrName, target); + + WebDataBinder dataBinder = new WebRequestDataBinder(null); + + WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class); + expect(binderFactory.createBinder(webRequest, target, expectedAttrName)).andReturn(dataBinder); + replay(binderFactory); + + processor.resolveArgument(param, model, webRequest, binderFactory); + + verify(binderFactory); + } + + @Test + public void createBinderWithAttributeConstructor() throws Exception { + WebDataBinder dataBinder = new WebRequestDataBinder(null); + + WebDataBinderFactory factory = createMock(WebDataBinderFactory.class); + expect(factory.createBinder((NativeWebRequest) anyObject(), notNull(), eq("attrName"))).andReturn(dataBinder); + replay(factory); + + processor.resolveArgument(annotatedParam, model, webRequest, factory); + + verify(factory); + } + + @Test + public void bindAndValidate() throws Exception { + Object target = new TestBean(); + model.addAttribute("attrName", target); + StubRequestDataBinder dataBinder = new StubRequestDataBinder(target); + + WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class); + expect(binderFactory.createBinder(webRequest, target, "attrName")).andReturn(dataBinder); + replay(binderFactory); + + processor.resolveArgument(annotatedParam, model, webRequest, binderFactory); + + assertTrue(dataBinder.isBindInvoked()); + assertTrue(dataBinder.isValidateInvoked()); + } + + @Test(expected=BindException.class) + public void bindAndFail() throws Exception { + Object target = new TestBean(); + model.addAttribute(target); + StubRequestDataBinder dataBinder = new StubRequestDataBinder(target); + dataBinder.getBindingResult().reject("error"); + + WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class); + expect(binderFactory.createBinder(webRequest, target, "testBean")).andReturn(dataBinder); + replay(binderFactory); + + processor.resolveArgument(notAnnotatedParam, model, webRequest, binderFactory); + } + + @SuppressWarnings("rawtypes") + @Test + public void handleAnnotatedReturnValue() throws Exception { + ModelAndViewContainer mavContainer = new ModelAndViewContainer(model); + processor.handleReturnValue("expected", annotatedReturnParam, mavContainer, webRequest); + + assertEquals("expected", mavContainer.getModel().get("modelAttrName")); + } + + @SuppressWarnings("rawtypes") + @Test + public void handleNotAnnotatedReturnValue() throws Exception { + TestBean testBean = new TestBean("expected"); + ModelAndViewContainer mavContainer = new ModelAndViewContainer(model); + processor.handleReturnValue(testBean, notAnnotatedReturnParam, mavContainer, webRequest); + + assertSame(testBean, mavContainer.getModel().get("testBean")); + } + + + @SessionAttributes(types=TestBean.class) + private static class ModelAttributeHandler { + + @SuppressWarnings("unused") + public void modelAttribute(@ModelAttribute("attrName") @Valid TestBean annotatedAttr, + Errors errors, + int intArg, + @ModelAttribute TestBean defaultNameAttr, + TestBean notAnnotatedAttr) { + } + } + + @SuppressWarnings("unused") + @ModelAttribute("modelAttrName") + private String annotatedReturnValue() { + return null; + } + + @SuppressWarnings("unused") + private TestBean notAnnotatedReturnValue() { + return null; + } + + private static class StubRequestDataBinder extends WebRequestDataBinder { + + private boolean bindInvoked; + + private boolean validateInvoked; + + public StubRequestDataBinder(Object target) { + super(target); + } + + public boolean isBindInvoked() { + return bindInvoked; + } + + public boolean isValidateInvoked() { + return validateInvoked; + } + + @Override + public void bind(WebRequest request) { + this.bindInvoked = true; + } + + @Override + public void validate() { + this.validateInvoked = true; + } + } + + @Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER }) + @Retention(RUNTIME) + public @interface Valid { + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelMethodProcessorTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelMethodProcessorTests.java new file mode 100644 index 0000000000..ffc844066c --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/ModelMethodProcessorTests.java @@ -0,0 +1,131 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Test fixture for {@link ModelMethodProcessor} unit tests. + * + * @author Rossen Stoyanchev + */ +public class ModelMethodProcessorTests { + + private ModelMethodProcessor resolver; + + private MethodParameter modelParameter; + + private MethodParameter modelReturnType; + + private MethodParameter mapParameter; + + private MethodParameter mapReturnType; + + private NativeWebRequest webRequest; + + @Before + public void setUp() throws Exception { + this.resolver = new ModelMethodProcessor(); + + Method modelMethod = getClass().getDeclaredMethod("model", Model.class); + this.modelParameter = new MethodParameter(modelMethod, 0); + this.modelReturnType = new MethodParameter(modelMethod, -1); + + Method mapMethod = getClass().getDeclaredMethod("map", Map.class); + this.mapParameter = new MethodParameter(mapMethod, 0); + this.mapReturnType = new MethodParameter(mapMethod, 0); + + this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + } + + @Test + public void usesResponseArgument() { + assertFalse(resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue(resolver.supportsParameter(modelParameter)); + assertTrue(resolver.supportsParameter(mapParameter)); + } + + @Test + public void supportsReturnType() { + assertTrue(resolver.supportsReturnType(modelReturnType)); + assertTrue(resolver.supportsReturnType(mapReturnType)); + } + + @Test + public void resolveArgumentValue() throws Exception { + ExtendedModelMap model = new ExtendedModelMap(); + + Object result = resolver.resolveArgument(modelParameter, model, webRequest, null); + assertSame(model, result); + + result = resolver.resolveArgument(mapParameter, model, webRequest, null); + assertSame(model, result); + } + + @SuppressWarnings("rawtypes") + @Test + public void handleReturnValue() throws Exception { + ExtendedModelMap implicitModel = new ExtendedModelMap(); + implicitModel.put("attr1", "value1"); + + ExtendedModelMap returnValue = new ExtendedModelMap(); + returnValue.put("attr2", "value2"); + + ModelAndViewContainer mavContainer = new ModelAndViewContainer(implicitModel); + resolver.handleReturnValue(returnValue , modelReturnType, mavContainer, webRequest); + ModelMap actualModel = mavContainer.getModel(); + assertEquals("value1", actualModel.get("attr1")); + assertEquals("value2", actualModel.get("attr2")); + + mavContainer = new ModelAndViewContainer(implicitModel); + resolver.handleReturnValue(returnValue , mapReturnType, mavContainer, webRequest); + actualModel = mavContainer.getModel(); + assertEquals("value1", actualModel.get("attr1")); + assertEquals("value2", actualModel.get("attr2")); + } + + @SuppressWarnings("unused") + private Model model(Model model) { + return null; + } + + @SuppressWarnings("unused") + private Map map(Map map) { + return null; + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..3c61fcbee3 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMapMethodArgumentResolverTests.java @@ -0,0 +1,140 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.RequestHeaderMapMethodArgumentResolver; + +/** + * @author Arjen Poutsma + */ +public class RequestHeaderMapMethodArgumentResolverTests { + + private RequestHeaderMapMethodArgumentResolver resolver; + + private MethodParameter mapParameter; + + private MethodParameter multiValueMapParameter; + + private MethodParameter httpHeadersParameter; + + private MockHttpServletRequest servletRequest; + + private MethodParameter unsupportedParameter; + + private NativeWebRequest webRequest; + + @Before + public void setUp() throws Exception { + resolver = new RequestHeaderMapMethodArgumentResolver(); + Method method = getClass() + .getMethod("params", Map.class, MultiValueMap.class, HttpHeaders.class, Map.class); + mapParameter = new MethodParameter(method, 0); + multiValueMapParameter = new MethodParameter(method, 1); + httpHeadersParameter = new MethodParameter(method, 2); + unsupportedParameter = new MethodParameter(method, 3); + + servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); + + } + + @Test + public void usesResponseArgument() throws NoSuchMethodException { + assertFalse("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue("Map parameter not supported", resolver.supportsParameter(mapParameter)); + assertTrue("MultiValueMap parameter not supported", resolver.supportsParameter(multiValueMapParameter)); + assertTrue("HttpHeaders parameter not supported", resolver.supportsParameter(httpHeadersParameter)); + assertFalse("non-@RequestParam map supported", resolver.supportsParameter(unsupportedParameter)); + } + + @Test + @SuppressWarnings("unchecked") + public void resolveMapArgument() throws Exception { + String headerName = "foo"; + String headerValue = "bar"; + Map expected = Collections.singletonMap(headerName, headerValue); + servletRequest.addHeader(headerName, headerValue); + + Map result = (Map) resolver.resolveArgument(mapParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + @SuppressWarnings("unchecked") + public void resolveMultiValueMapArgument() throws Exception { + String headerName = "foo"; + String headerValue1 = "bar"; + String headerValue2 = "baz"; + MultiValueMap expected = new LinkedMultiValueMap(1); + expected.add(headerName, headerValue1); + expected.add(headerName, headerValue2); + servletRequest.addHeader(headerName, headerValue1); + servletRequest.addHeader(headerName, headerValue2); + + MultiValueMap result = + (MultiValueMap) resolver.resolveArgument(multiValueMapParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveHttpHeadersArgument() throws Exception { + String headerName = "foo"; + String headerValue1 = "bar"; + String headerValue2 = "baz"; + HttpHeaders expected = new HttpHeaders(); + expected.add(headerName, headerValue1); + expected.add(headerName, headerValue2); + servletRequest.addHeader(headerName, headerValue1); + servletRequest.addHeader(headerName, headerValue2); + + HttpHeaders result = (HttpHeaders) resolver.resolveArgument(httpHeadersParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + public void params(@RequestHeader Map param1, + @RequestHeader MultiValueMap param2, + @RequestHeader HttpHeaders param3, + Map unsupported) { + + } + + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolverTests.java new file mode 100644 index 0000000000..d72a9f4e82 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestHeaderMethodArgumentResolverTests.java @@ -0,0 +1,151 @@ +/* + * 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.method.annotation.support.RequestHeaderMethodArgumentResolver; + +/** + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public class RequestHeaderMethodArgumentResolverTests { + + private RequestHeaderMethodArgumentResolver resolver; + + private MethodParameter stringParameter; + + private MethodParameter stringArrayParameter; + + private MethodParameter systemPropertyParameter; + + private MethodParameter contextPathParameter; + + private MethodParameter otherParameter; + + private MockHttpServletRequest servletRequest; + + private NativeWebRequest webRequest; + + @Before + public void setUp() throws Exception { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.refresh(); + + resolver = new RequestHeaderMethodArgumentResolver(context.getBeanFactory()); + + Method method = getClass().getMethod("params", String.class, String[].class, String.class, String.class, Map.class); + stringParameter = new MethodParameter(method, 0); + stringArrayParameter = new MethodParameter(method, 1); + systemPropertyParameter = new MethodParameter(method, 2); + contextPathParameter = new MethodParameter(method, 3); + otherParameter = new MethodParameter(method, 4); + + servletRequest = new MockHttpServletRequest(); + webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); + + // Expose request to the current thread (for SpEL expressions) + RequestContextHolder.setRequestAttributes(webRequest); + } + + @After + public void teardown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void usesResponseArgument() throws NoSuchMethodException { + assertFalse("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue("String parameter not supported", resolver.supportsParameter(stringParameter)); + assertTrue("String array parameter not supported", resolver.supportsParameter(stringArrayParameter)); + assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(otherParameter)); + } + + @Test + public void resolveStringArgument() throws Exception { + String expected = "foo"; + servletRequest.addHeader("name", expected); + + String result = (String) resolver.resolveArgument(stringParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveStringArrayArgument() throws Exception { + String[] expected = new String[]{"foo", "bar"}; + servletRequest.addHeader("name", expected); + + String[] result = (String[]) resolver.resolveArgument(stringArrayParameter, null, webRequest, null); + assertArrayEquals("Invalid result", expected, result); + } + + @Test + public void resolveDefaultValue() throws Exception { + String result = (String) resolver.resolveArgument(stringParameter, null, webRequest, null); + assertEquals("Invalid result", "bar", result); + } + + @Test + public void resolveDefaultValueFromSystemProperty() throws Exception { + System.setProperty("header", "bar"); + String result = (String) resolver.resolveArgument(systemPropertyParameter, null, webRequest, null); + assertEquals("bar", result); + } + + @Test + public void resolveDefaultValueFromRequest() throws Exception { + servletRequest.setContextPath("/bar"); + String result = (String) resolver.resolveArgument(contextPathParameter, null, webRequest, null); + assertEquals("/bar", result); + } + + @Test(expected = IllegalStateException.class) + public void notFound() throws Exception { + String result = (String) resolver.resolveArgument(stringArrayParameter, null, webRequest, null); + assertEquals("Invalid result", "bar", result); + } + + public void params(@RequestHeader(value = "name", defaultValue = "bar") String param1, + @RequestHeader("name") String[] param2, + @RequestHeader(value = "name", defaultValue="#{systemProperties.header}") String param3, + @RequestHeader(value = "name", defaultValue="#{request.contextPath}") String param4, + @RequestHeader("name") Map unsupported) { + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..a726603b99 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMapMethodArgumentResolverTests.java @@ -0,0 +1,118 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.RequestParamMapMethodArgumentResolver; + +/** + * @author Arjen Poutsma + */ +public class RequestParamMapMethodArgumentResolverTests { + + private RequestParamMapMethodArgumentResolver resolver; + + private MethodParameter mapParameter; + + private MethodParameter multiValueMapParameter; + + private MockHttpServletRequest servletRequest; + + private NativeWebRequest webRequest; + + private MethodParameter unsupportedParameter; + + @Before + public void setUp() throws Exception { + resolver = new RequestParamMapMethodArgumentResolver(); + Method method = getClass() + .getMethod("params", Map.class, MultiValueMap.class, Map.class); + mapParameter = new MethodParameter(method, 0); + multiValueMapParameter = new MethodParameter(method, 1); + unsupportedParameter = new MethodParameter(method, 2); + + servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); + + } + + @Test + public void usesResponseArgument() throws NoSuchMethodException { + assertFalse("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue("Map parameter not supported", resolver.supportsParameter(mapParameter)); + assertTrue("MultiValueMap parameter not supported", resolver.supportsParameter(multiValueMapParameter)); + assertFalse("non-@RequestParam map supported", resolver.supportsParameter(unsupportedParameter)); + } + + @Test + @SuppressWarnings("unchecked") + public void resolveMapArgument() throws Exception { + String headerName = "foo"; + String headerValue = "bar"; + Map expected = Collections.singletonMap(headerName, headerValue); + servletRequest.addParameter(headerName, headerValue); + + Map result = (Map) resolver.resolveArgument(mapParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + @SuppressWarnings("unchecked") + public void resolveMultiValueMapArgument() throws Exception { + String headerName = "foo"; + String headerValue1 = "bar"; + String headerValue2 = "baz"; + MultiValueMap expected = new LinkedMultiValueMap(1); + expected.add(headerName, headerValue1); + expected.add(headerName, headerValue2); + servletRequest.addParameter(headerName, new String[]{headerValue1, headerValue2}); + + MultiValueMap result = + (MultiValueMap) resolver.resolveArgument(multiValueMapParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + public void params(@RequestParam Map param1, + @RequestParam MultiValueMap param2, + Map unsupported) { + + } + + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java new file mode 100644 index 0000000000..43df1086ad --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java @@ -0,0 +1,161 @@ +/* + * 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.RequestParamMethodArgumentResolver; +import org.springframework.web.multipart.MultipartFile; + +/** + * @author Arjen Poutsma + */ +public class RequestParamMethodArgumentResolverTests { + + private RequestParamMethodArgumentResolver resolver; + + private MethodParameter stringParameter; + + private MethodParameter stringArrayParameter; + + private MethodParameter mapParameter; + + private MethodParameter fileParameter; + + private MethodParameter otherParameter; + + private MockHttpServletRequest servletRequest; + + private NativeWebRequest webRequest; + + private MethodParameter plainParameter; + + @Before + public void setUp() throws Exception { + resolver = new RequestParamMethodArgumentResolver(null, true); + Method method = getClass() + .getMethod("params", String.class, String[].class, Map.class, MultipartFile.class, Map.class, String.class); + stringParameter = new MethodParameter(method, 0); + stringArrayParameter = new MethodParameter(method, 1); + mapParameter = new MethodParameter(method, 2); + fileParameter = new MethodParameter(method, 3); + otherParameter = new MethodParameter(method, 4); + plainParameter = new MethodParameter(method, 5); + + plainParameter.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + + servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); + + } + + @Test + public void usesResponseArgument() throws NoSuchMethodException { + assertFalse("resolver uses response argument", resolver.usesResponseArgument(null)); + } + + @Test + public void supportsParameter() { + assertTrue("String parameter not supported", resolver.supportsParameter(stringParameter)); + assertTrue("String array parameter not supported", resolver.supportsParameter(stringArrayParameter)); + assertTrue("Named map not parameter supported", resolver.supportsParameter(mapParameter)); + assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(fileParameter)); + assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(otherParameter)); + assertTrue("Simple type params supported w/o annotations", resolver.supportsParameter(plainParameter)); + + resolver = new RequestParamMethodArgumentResolver(null, false); + assertFalse(resolver.supportsParameter(plainParameter)); + } + + @Test + public void resolveStringArgument() throws Exception { + String expected = "foo"; + servletRequest.addParameter("name", expected); + + String result = (String) resolver.resolveArgument(stringParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveStringArrayArgument() throws Exception { + String[] expected = new String[]{"foo", "bar"}; + servletRequest.addParameter("name", expected); + + String[] result = (String[]) resolver.resolveArgument(stringArrayParameter, null, webRequest, null); + assertArrayEquals("Invalid result", expected, result); + } + + @Test + public void resolveMultipartFileArgument() throws Exception { + MockMultipartHttpServletRequest servletRequest = new MockMultipartHttpServletRequest(); + MultipartFile expected = new MockMultipartFile("file", "Hello World".getBytes()); + servletRequest.addFile(expected); + webRequest = new ServletWebRequest(servletRequest); + + MultipartFile result = (MultipartFile) resolver.resolveArgument(fileParameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveDefaultValue() throws Exception { + String result = (String) resolver.resolveArgument(stringParameter, null, webRequest, null); + assertEquals("Invalid result", "bar", result); + } + + @Test(expected = MissingServletRequestParameterException.class) + public void notFound() throws Exception { + String result = (String) resolver.resolveArgument(stringArrayParameter, null, webRequest, null); + assertEquals("Invalid result", "bar", result); + } + + @Test + public void resolveSimpleTypeParam() throws Exception { + servletRequest.setParameter("plainParam", "plainValue"); + String result = (String) resolver.resolveArgument(plainParameter, null, webRequest, null); + assertEquals("plainValue", result); + } + + public void params(@RequestParam(value = "name", defaultValue = "bar") String param1, + @RequestParam("name") String[] param2, + @RequestParam("name") Map param3, + @RequestParam(value = "file") MultipartFile file, + @RequestParam Map unsupported, + String plainParam) { + + } + + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapterTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapterTests.java new file mode 100644 index 0000000000..182a4d8922 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/WebArgumentResolverAdapterTests.java @@ -0,0 +1,165 @@ +/* + * 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 static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.support.WebArgumentResolverAdapter; + +/** + * @author Arjen Poutsma + */ +public class WebArgumentResolverAdapterTests { + + private WebArgumentResolver adaptee; + + private WebArgumentResolverAdapter adapter; + + private MethodParameter parameter; + + private NativeWebRequest webRequest; + + @Before + public void setUp() throws Exception { + adaptee = createMock(WebArgumentResolver.class); + adapter = new WebArgumentResolverAdapter(adaptee); + + parameter = new MethodParameter(getClass().getMethod("handle", Integer.TYPE), 0); + + webRequest = new ServletWebRequest(new MockHttpServletRequest()); + RequestContextHolder.setRequestAttributes(webRequest); + } + + @After + public void resetRequestContextHolder() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void supportsParameter() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andReturn(42); + + replay(adaptee); + + boolean result = adapter.supportsParameter(parameter); + assertTrue("Parameter not supported", result); + + verify(adaptee); + } + + @Test + public void supportsParameterUnresolved() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andReturn(WebArgumentResolver.UNRESOLVED); + + replay(adaptee); + + boolean result = adapter.supportsParameter(parameter); + assertFalse("Parameter supported", result); + + verify(adaptee); + } + + @Test + public void supportsParameterWrongType() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andReturn("Foo"); + + replay(adaptee); + + boolean result = adapter.supportsParameter(parameter); + assertFalse("Parameter supported", result); + + verify(adaptee); + } + + @Test + public void supportsParameterThrowsException() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andThrow(new Exception()); + + replay(adaptee); + + boolean result = adapter.supportsParameter(parameter); + assertFalse("Parameter supported", result); + + verify(adaptee); + } + + @Test + public void resolveArgument() throws Exception { + int expected = 42; + expect(adaptee.resolveArgument(parameter, webRequest)).andReturn(expected); + + replay(adaptee); + + Object result = adapter.resolveArgument(parameter, null, webRequest, null); + assertEquals("Invalid result", expected, result); + + verify(adaptee); + + } + + @Test(expected = IllegalStateException.class) + public void resolveArgumentUnresolved() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andReturn(WebArgumentResolver.UNRESOLVED); + + replay(adaptee); + + adapter.resolveArgument(parameter, null, webRequest, null); + + verify(adaptee); + } + + @Test(expected = IllegalStateException.class) + public void resolveArgumentWrongType() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andReturn("Foo"); + + replay(adaptee); + + adapter.resolveArgument(parameter, null, webRequest, null); + + verify(adaptee); + } + + @Test(expected = Exception.class) + public void resolveArgumentThrowsException() throws Exception { + expect(adaptee.resolveArgument(parameter, webRequest)).andThrow(new Exception()); + + replay(adaptee); + + adapter.resolveArgument(parameter, null, webRequest, null); + + verify(adaptee); + } + + public void handle(int param) { + + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainerTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainerTests.java new file mode 100644 index 0000000000..fbed71ba33 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodArgumentResolverContainerTests.java @@ -0,0 +1,103 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; + +/** + * Test fixture for {@link HandlerMethodArgumentResolverContainer} unit tests. + * + * @author Rossen Stoyanchev + */ +public class HandlerMethodArgumentResolverContainerTests { + + private HandlerMethodArgumentResolverContainer container; + + private MethodParameter paramInteger; + + private MethodParameter paramString; + + @Before + public void setUp() throws Exception { + this.container = new HandlerMethodArgumentResolverContainer(); + + Method method = getClass().getDeclaredMethod("handle", Integer.class, String.class); + this.paramInteger = new MethodParameter(method, 0); + this.paramString = new MethodParameter(method, 1); + } + + @Test + public void supportsParameter() throws Exception { + registerResolver(Integer.class, null, false); + + assertTrue(this.container.supportsParameter(paramInteger)); + assertFalse(this.container.supportsParameter(paramString)); + } + + @Test + public void resolveArgument() throws Exception { + registerResolver(Integer.class, Integer.valueOf(55), false); + Object resolvedValue = this.container.resolveArgument(paramInteger, null, null, null); + + assertEquals(Integer.valueOf(55), resolvedValue); + } + + @Test + public void resolveArgumentMultipleResolvers() throws Exception { + registerResolver(Integer.class, Integer.valueOf(1), false); + registerResolver(Integer.class, Integer.valueOf(2), false); + Object resolvedValue = this.container.resolveArgument(paramInteger, null, null, null); + + assertEquals("Didn't use the first registered resolver", Integer.valueOf(1), resolvedValue); + } + + @Test(expected=IllegalStateException.class) + public void noSuitableArgumentResolver() throws Exception { + this.container.resolveArgument(paramString, null, null, null); + } + + @Test + public void argResolverUsesResponse() throws Exception { + registerResolver(Integer.class, null, true); + assertTrue(this.container.usesResponseArgument(paramInteger)); + } + + @Test + public void argResolverDoesntUseResponse() throws Exception { + registerResolver(Integer.class, null, false); + assertFalse(this.container.usesResponseArgument(paramInteger)); + } + + protected StubArgumentResolver registerResolver(Class supportedType, Object stubValue, boolean usesResponse) { + StubArgumentResolver resolver = new StubArgumentResolver(supportedType, stubValue, usesResponse); + this.container.registerArgumentResolver(resolver); + return resolver; + } + + @SuppressWarnings("unused") + private void handle(Integer arg1, String arg2) { + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainerTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainerTests.java new file mode 100644 index 0000000000..c5301f144c --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerContainerTests.java @@ -0,0 +1,113 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; + +/** + * Test fixture for {@link HandlerMethodReturnValueHandlerContainer} unit tests. + * + * @author Rossen Stoyanchev + */ +public class HandlerMethodReturnValueHandlerContainerTests { + + private HandlerMethodReturnValueHandlerContainer container; + + ModelAndViewContainer mavContainer; + + private MethodParameter paramInteger; + + private MethodParameter paramString; + + @Before + public void setUp() throws Exception { + this.container = new HandlerMethodReturnValueHandlerContainer(); + + this.paramInteger = new MethodParameter(getClass().getDeclaredMethod("handleInteger"), -1); + this.paramString = new MethodParameter(getClass().getDeclaredMethod("handleString"), -1); + + mavContainer = new ModelAndViewContainer(null); + } + + @Test + public void supportsReturnType() throws Exception { + registerReturnValueHandler(Integer.class, false); + + assertTrue(this.container.supportsReturnType(paramInteger)); + assertFalse(this.container.supportsReturnType(paramString)); + } + + @Test + public void handleReturnValue() throws Exception { + StubReturnValueHandler handler = registerReturnValueHandler(Integer.class, false); + this.container.handleReturnValue(Integer.valueOf(55), paramInteger, mavContainer, null); + + assertEquals(Integer.valueOf(55), handler.getUnhandledReturnValue()); + } + + @Test + public void handleReturnValueMultipleHandlers() throws Exception { + StubReturnValueHandler handler1 = registerReturnValueHandler(Integer.class, false); + StubReturnValueHandler handler2 = registerReturnValueHandler(Integer.class, false); + this.container.handleReturnValue(Integer.valueOf(55), paramInteger, mavContainer, null); + + assertEquals("Didn't use the 1st registered handler", Integer.valueOf(55), handler1.getUnhandledReturnValue()); + assertNull("Shouldn't have use the 2nd registered handler", handler2.getUnhandledReturnValue()); + } + + @Test(expected=IllegalStateException.class) + public void noSuitableReturnValueHandler() throws Exception { + registerReturnValueHandler(Integer.class, false); + this.container.handleReturnValue("value", paramString, null, null); + } + + @Test + public void returnValueHandlerUsesResponse() throws Exception { + registerReturnValueHandler(Integer.class, true); + assertTrue(this.container.usesResponseArgument(paramInteger)); + } + + @Test + public void returnValueHandlerDosntUseResponse() throws Exception { + registerReturnValueHandler(Integer.class, false); + assertFalse(this.container.usesResponseArgument(paramInteger)); + } + + protected StubReturnValueHandler registerReturnValueHandler(Class returnType, boolean usesResponse) { + StubReturnValueHandler handler = new StubReturnValueHandler(returnType, usesResponse); + this.container.registerReturnValueHandler(handler); + return handler; + } + + @SuppressWarnings("unused") + private Integer handleInteger() { + return null; + } + + @SuppressWarnings("unused") + private String handleString() { + return null; + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java new file mode 100644 index 0000000000..78ee1ccf8e --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java @@ -0,0 +1,131 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * Test fixture for {@link InvocableHandlerMethod} unit tests. + * + * @author Rossen Stoyanchev + */ +public class InvocableHandlerMethodTests { + + private HandlerMethodArgumentResolverContainer argResolvers; + + private NativeWebRequest webRequest; + + private MockHttpServletResponse response; + + @Before + public void setUp() throws Exception { + argResolvers = new HandlerMethodArgumentResolverContainer(); + + response = new MockHttpServletResponse(); + this.webRequest = new ServletWebRequest(new MockHttpServletRequest(), response); + } + + @Test + public void argResolutionAndReturnValueHandling() throws Exception { + StubArgumentResolver resolver0 = registerResolver(Integer.class, 99, false); + StubArgumentResolver resolver1 = registerResolver(String.class, "value", false); + + InvocableHandlerMethod method = handlerMethod(new Handler(), "handle", Integer.class, String.class); + Object returnValue = method.invokeForRequest(webRequest, null); + + assertEquals("Integer resolver not invoked", 1, resolver0.getResolvedParameterNames().size()); + assertEquals("String resolver not invoked", 1, resolver1.getResolvedParameterNames().size()); + assertEquals("Invalid return value", "99-value", returnValue); + } + + @Test + public void providedArgResolution() throws Exception { + InvocableHandlerMethod method = handlerMethod(new Handler(), "handle", Integer.class, String.class); + Object returnValue = method.invokeForRequest(webRequest, null, 99, "value"); + + assertEquals("Expected raw return value with no handlers registered", String.class, returnValue.getClass()); + assertEquals("Provided argument values were not resolved", "99-value", returnValue); + } + + @Test + public void parameterNameDiscovery() throws Exception { + StubArgumentResolver resolver = registerResolver(Integer.class, 99, false); + + InvocableHandlerMethod method = handlerMethod(new Handler(), "parameterNameDiscovery", Integer.class); + method.invokeForRequest(webRequest, null); + + assertEquals("intArg", resolver.getResolvedParameterNames().get(0).getParameterName()); + } + + @Test + public void usesResponseArgResolver() throws Exception { + InvocableHandlerMethod requestMethod = handlerMethod(new Handler(), "usesResponse", int.class); + registerResolver(int.class, 99, true); + + assertTrue(requestMethod.usesResponseArgument()); + } + + private InvocableHandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) + throws Exception { + Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); + handlerMethod.setArgumentResolverContainer(argResolvers); + return handlerMethod; + } + + private StubArgumentResolver registerResolver(Class supportedType, Object stubValue, boolean usesResponse) { + StubArgumentResolver resolver = new StubArgumentResolver(supportedType, stubValue, usesResponse); + argResolvers.registerArgumentResolver(resolver); + return resolver; + } + + private static class Handler { + + @SuppressWarnings("unused") + public String handle(Integer intArg, String stringArg) { + return intArg + "-" + stringArg; + } + + @SuppressWarnings("unused") + @RequestMapping + public void parameterNameDiscovery(Integer intArg) { + } + + @SuppressWarnings("unused") + @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "400 Bad Request") + public void responseStatus() { + } + + @SuppressWarnings("unused") + public String usesResponse(int arg) { + return ""; + } + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/support/StubArgumentResolver.java b/org.springframework.web/src/test/java/org/springframework/web/method/support/StubArgumentResolver.java new file mode 100644 index 0000000000..7f9cc12ee4 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/support/StubArgumentResolver.java @@ -0,0 +1,67 @@ +/* + * 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 org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +/** + * Resolves a method argument from a stub value. Records all resolved parameters. + * + * @author Rossen Stoyanchev + */ +public class StubArgumentResolver implements HandlerMethodArgumentResolver { + + private final Class supportedType; + + private final Object stubValue; + + private final boolean usesResponse; + + private List resolvedParameters = new ArrayList(); + + public StubArgumentResolver(Class supportedType, Object stubValue, boolean usesResponse) { + this.supportedType = supportedType; + this.stubValue = stubValue; + this.usesResponse = usesResponse; + } + + public List getResolvedParameterNames() { + return resolvedParameters; + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return this.usesResponse; + } + + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(this.supportedType); + } + + public Object resolveArgument(MethodParameter parameter, ModelMap model, NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + this.resolvedParameters.add(parameter); + return this.stubValue; + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/support/StubReturnValueHandler.java b/org.springframework.web/src/test/java/org/springframework/web/method/support/StubReturnValueHandler.java new file mode 100644 index 0000000000..7dea9b3bea --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/method/support/StubReturnValueHandler.java @@ -0,0 +1,63 @@ +/* + * 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.Conventions; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * Handles a return value by adding it as a model attribute with a default name. + * Records the raw return value (before handling). + * + * @author Rossen Stoyanchev + */ +public class StubReturnValueHandler implements HandlerMethodReturnValueHandler { + + private final Class supportedReturnType; + + private final boolean usesResponse; + + private Object unhandledReturnValue; + + public StubReturnValueHandler(Class supportedReturnType, boolean usesResponse) { + this.supportedReturnType = supportedReturnType; + this.usesResponse = usesResponse; + } + + public Object getUnhandledReturnValue() { + return this.unhandledReturnValue; + } + + public boolean supportsReturnType(MethodParameter returnType) { + return returnType.getParameterType().equals(this.supportedReturnType); + } + + public void handleReturnValue(Object returnValue, + MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + this.unhandledReturnValue = returnValue; + if (returnValue != null) { + mavContainer.addModelAttribute(Conventions.getVariableName(returnValue), returnValue); + } + } + + public boolean usesResponseArgument(MethodParameter parameter) { + return this.usesResponse; + } +}