diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/DispatcherPortlet.properties b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/DispatcherPortlet.properties index 68c701b5536..35e056826cf 100644 --- a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/DispatcherPortlet.properties +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/DispatcherPortlet.properties @@ -7,4 +7,6 @@ org.springframework.web.portlet.HandlerMapping=org.springframework.web.portlet.m org.springframework.web.portlet.HandlerAdapter=org.springframework.web.portlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter +org.springframework.web.portlet.HandlerExceptionResolver=org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver + org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/AbstractHandlerExceptionResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/AbstractHandlerExceptionResolver.java new file mode 100644 index 00000000000..37bae30edc9 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/AbstractHandlerExceptionResolver.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2009 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.portlet.handler; + +import java.util.Set; +import javax.portlet.PortletRequest; +import javax.portlet.WindowState; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; +import javax.portlet.MimeResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.web.portlet.HandlerExceptionResolver; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.core.Ordered; + +/** + * Abstract base class for {@link HandlerExceptionResolver} implementations.

Provides a set of mapped handlers that + * the resolver should map to, and the {@link Ordered} implementation. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private int order = Ordered.LOWEST_PRECEDENCE; + + private Set mappedHandlers; + + private Class[] mappedHandlerClasses; + + private Log warnLogger; + + private boolean renderWhenMinimized = false; + + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return this.order; + } + + /** + * Specify the set of handlers that this exception resolver should map. + * The exception mappings and the default error view will only apply + * to the specified handlers. + *

If no handlers set, both the exception mappings and the default error + * view will apply to all handlers. This means that a specified default + * error view will be used as fallback for all exceptions; any further + * HandlerExceptionResolvers in the chain will be ignored in this case. + */ + public void setMappedHandlers(Set mappedHandlers) { + this.mappedHandlers = mappedHandlers; + } + + /** + * Specify the set of classes that this exception resolver should apply to. + * The exception mappings and the default error view will only apply + * to handlers of the specified type; the specified types may be interfaces + * and superclasses of handlers as well. + *

If no handlers and handler classes are set, the exception mappings + * and the default error view will apply to all handlers. This means that + * a specified default error view will be used as fallback for all exceptions; + * any further HandlerExceptionResolvers in the chain will be ignored in + * this case. + */ + public void setMappedHandlerClasses(Class[] mappedHandlerClasses) { + this.mappedHandlerClasses = mappedHandlerClasses; + } + + /** + * Set the log category for warn logging. The name will be passed to the + * underlying logger implementation through Commons Logging, getting + * interpreted as log category according to the logger's configuration. + *

Default is no warn logging. Specify this setting to activate + * warn logging into a specific category. Alternatively, override + * the {@link #logException} method for custom logging. + * @see org.apache.commons.logging.LogFactory#getLog(String) + * @see org.apache.log4j.Logger#getLogger(String) + * @see java.util.logging.Logger#getLogger(String) + */ + public void setWarnLogCategory(String loggerName) { + this.warnLogger = LogFactory.getLog(loggerName); + } + + /** + * Set if the resolver should render a view when the portlet is in + * a minimized window. The default is "false". + * @see javax.portlet.RenderRequest#getWindowState() + * @see javax.portlet.WindowState#MINIMIZED + */ + public void setRenderWhenMinimized(boolean renderWhenMinimized) { + this.renderWhenMinimized = renderWhenMinimized; + } + + /** + * Checks whether this resolver is supposed to apply (i.e. the handler + * matches in case of "mappedHandlers" having been specified), then + * delegates to the {@link #doResolveException} template method. + */ + public ModelAndView resolveException( + RenderRequest request, RenderResponse response, Object handler, Exception ex) { + + if (shouldApplyTo(request, handler)) { + return doResolveException(request, response, handler, ex); + } + else { + return null; + } + } + + public ModelAndView resolveException( + ResourceRequest request, ResourceResponse response, Object handler, Exception ex) { + + if (shouldApplyTo(request, handler)) { + return doResolveException(request, response, handler, ex); + } + else { + return null; + } + } + + /** + * Check whether this resolver is supposed to apply to the given handler. + *

The default implementation checks against the specified mapped handlers + * and handler classes, if any, and alspo checks the window state (according + * to the "renderWhenMinimize" property). + * @param request current portlet request + * @param handler the executed handler, or null if none chosen at the + * time of the exception (for example, if multipart resolution failed) + * @return whether this resolved should proceed with resolving the exception + * for the given request and handler + * @see #setMappedHandlers + * @see #setMappedHandlerClasses + */ + protected boolean shouldApplyTo(PortletRequest request, Object handler) { + // If the portlet is minimized and we don't want to render then return null. + if (WindowState.MINIMIZED.equals(request.getWindowState()) && !this.renderWhenMinimized) { + return false; + } + // Check mapped handlers... + if (handler != null) { + if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) { + return true; + } + if (this.mappedHandlerClasses != null) { + for (Class mappedClass : this.mappedHandlerClasses) { + if (mappedClass.isInstance(handler)) { + return true; + } + } + } + } + // Else only apply if there are no explicit handler mappings. + return (this.mappedHandlers == null && this.mappedHandlerClasses == null); + } + + /** + * Log the given exception at warn level, provided that warn logging has been + * activated through the {@link #setWarnLogCategory "warnLogCategory"} property. + *

Calls {@link #buildLogMessage} in order to determine the concrete message + * to log. Always passes the full exception to the logger. + * @param ex the exception that got thrown during handler execution + * @param request current portlet request (useful for obtaining metadata) + * @see #setWarnLogCategory + * @see #buildLogMessage + * @see org.apache.commons.logging.Log#warn(Object, Throwable) + */ + protected void logException(Exception ex, PortletRequest request) { + if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) { + this.warnLogger.warn(buildLogMessage(ex, request), ex); + } + } + + /** + * Build a log message for the given exception, occured during processing + * the given request. + * @param ex the exception that got thrown during handler execution + * @param request current portlet request (useful for obtaining metadata) + * @return the log message to use + */ + protected String buildLogMessage(Exception ex, PortletRequest request) { + return "Handler execution resulted in exception"; + } + + /** + * Actually resolve the given exception that got thrown during on handler execution, + * returning a ModelAndView that represents a specific error page if appropriate. + * + *

Must 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 portlet request + * @param response current portlet response + * @param handler the executed handler, 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 abstract ModelAndView doResolveException(PortletRequest request, + MimeResponse response, + Object handler, + Exception ex); + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/SimpleMappingExceptionResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/SimpleMappingExceptionResolver.java index 9bb2f51b678..accdd3adfcc 100644 --- a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/SimpleMappingExceptionResolver.java +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/handler/SimpleMappingExceptionResolver.java @@ -18,20 +18,9 @@ package org.springframework.web.portlet.handler; import java.util.Enumeration; import java.util.Properties; -import java.util.Set; import javax.portlet.MimeResponse; import javax.portlet.PortletRequest; -import javax.portlet.RenderRequest; -import javax.portlet.RenderResponse; -import javax.portlet.ResourceRequest; -import javax.portlet.ResourceResponse; -import javax.portlet.WindowState; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.Ordered; -import org.springframework.web.portlet.HandlerExceptionResolver; import org.springframework.web.portlet.ModelAndView; /** @@ -45,97 +34,22 @@ import org.springframework.web.portlet.ModelAndView; * * @author Juergen Hoeller * @author John A. Lewis + * @author Arjen Poutsma * @since 2.0 */ -public class SimpleMappingExceptionResolver implements HandlerExceptionResolver, Ordered { +public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver { /** * The default name of the exception attribute: "exception". */ public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception"; - - /** Logger available to subclasses */ - protected final Log logger = LogFactory.getLog(getClass()); - - private int order = Integer.MAX_VALUE; // default: same as non-Ordered - - private Set mappedHandlers; - - private Class[] mappedHandlerClasses; - - private boolean renderWhenMinimized = false; - - private Log warnLogger; - private Properties exceptionMappings; private String defaultErrorView; private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE; - - public void setOrder(int order) { - this.order = order; - } - - public int getOrder() { - return this.order; - } - - /** - * Specify the set of handlers that this exception resolver should map. - * The exception mappings and the default error view will only apply - * to the specified handlers. - *

If no handlers set, both the exception mappings and the default error - * view will apply to all handlers. This means that a specified default - * error view will be used as fallback for all exceptions; any further - * HandlerExceptionResolvers in the chain will be ignored in this case. - */ - public void setMappedHandlers(Set mappedHandlers) { - this.mappedHandlers = mappedHandlers; - } - - /** - * Specify the set of classes that this exception resolver should apply to. - * The exception mappings and the default error view will only apply - * to handlers of the specified type; the specified types may be interfaces - * and superclasses of handlers as well. - *

If no handlers and handler classes are set, the exception mappings - * and the default error view will apply to all handlers. This means that - * a specified default error view will be used as fallback for all exceptions; - * any further HandlerExceptionResolvers in the chain will be ignored in - * this case. - */ - public void setMappedHandlerClasses(Class[] mappedHandlerClasses) { - this.mappedHandlerClasses = mappedHandlerClasses; - } - - /** - * Set if the resolver should render a view when the portlet is in - * a minimized window. The default is "false". - * @see javax.portlet.RenderRequest#getWindowState() - * @see javax.portlet.WindowState#MINIMIZED - */ - public void setRenderWhenMinimized(boolean renderWhenMinimized) { - this.renderWhenMinimized = renderWhenMinimized; - } - - /** - * Set the log category for warn logging. The name will be passed to the - * underlying logger implementation through Commons Logging, getting - * interpreted as log category according to the logger's configuration. - *

Default is no warn logging. Specify this setting to activate - * warn logging into a specific category. Alternatively, override - * the {@link #logException} method for custom logging. - * @see org.apache.commons.logging.LogFactory#getLog(String) - * @see org.apache.log4j.Logger#getLogger(String) - * @see java.util.logging.Logger#getLogger(String) - */ - public void setWarnLogCategory(String loggerName) { - this.warnLogger = LogFactory.getLog(loggerName); - } - /** * Set the mappings between exception class names and error view names. * The exception class name can be a substring, with no wildcard support @@ -176,70 +90,6 @@ public class SimpleMappingExceptionResolver implements HandlerExceptionResolver, this.exceptionAttribute = exceptionAttribute; } - - /** - * Checks whether this resolver is supposed to apply (i.e. the handler - * matches in case of "mappedHandlers" having been specified), then - * delegates to the {@link #doResolveException} template method. - */ - public ModelAndView resolveException( - RenderRequest request, RenderResponse response, Object handler, Exception ex) { - - if (shouldApplyTo(request, handler)) { - return doResolveException(request, response, handler, ex); - } - else { - return null; - } - } - - public ModelAndView resolveException( - ResourceRequest request, ResourceResponse response, Object handler, Exception ex) { - - if (shouldApplyTo(request, handler)) { - return doResolveException(request, response, handler, ex); - } - else { - return null; - } - } - - - /** - * Check whether this resolver is supposed to apply to the given handler. - *

The default implementation checks against the specified mapped handlers - * and handler classes, if any, and alspo checks the window state (according - * to the "renderWhenMinimize" property). - * @param request current portlet request - * @param handler the executed handler, or null if none chosen at the - * time of the exception (for example, if multipart resolution failed) - * @return whether this resolved should proceed with resolving the exception - * for the given request and handler - * @see #setMappedHandlers - * @see #setMappedHandlerClasses - */ - protected boolean shouldApplyTo(PortletRequest request, Object handler) { - // If the portlet is minimized and we don't want to render then return null. - if (WindowState.MINIMIZED.equals(request.getWindowState()) && !this.renderWhenMinimized) { - return false; - } - // Check mapped handlers... - if (handler != null) { - if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) { - return true; - } - if (this.mappedHandlerClasses != null) { - for (Class mappedClass : this.mappedHandlerClasses) { - if (mappedClass.isInstance(handler)) { - return true; - } - } - } - } - // Else only apply if there are no explicit handler mappings. - return (this.mappedHandlers == null && this.mappedHandlerClasses == null); - } - /** * Actually resolve the given exception that got thrown during on handler execution, * returning a ModelAndView that represents a specific error page if appropriate. @@ -250,6 +100,7 @@ public class SimpleMappingExceptionResolver implements HandlerExceptionResolver, * @param ex the exception that got thrown during handler execution * @return a corresponding ModelAndView to forward to, or null for default processing */ + @Override protected ModelAndView doResolveException( PortletRequest request, MimeResponse response, Object handler, Exception ex) { @@ -269,36 +120,6 @@ public class SimpleMappingExceptionResolver implements HandlerExceptionResolver, } } - - /** - * Log the given exception at warn level, provided that warn logging has been - * activated through the {@link #setWarnLogCategory "warnLogCategory"} property. - *

Calls {@link #buildLogMessage} in order to determine the concrete message - * to log. Always passes the full exception to the logger. - * @param ex the exception that got thrown during handler execution - * @param request current portlet request (useful for obtaining metadata) - * @see #setWarnLogCategory - * @see #buildLogMessage - * @see org.apache.commons.logging.Log#warn(Object, Throwable) - */ - protected void logException(Exception ex, PortletRequest request) { - if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) { - this.warnLogger.warn(buildLogMessage(ex, request), ex); - } - } - - /** - * Build a log message for the given exception, occured during processing - * the given request. - * @param ex the exception that got thrown during handler execution - * @param request current portlet request (useful for obtaining metadata) - * @return the log message to use - */ - protected String buildLogMessage(Exception ex, PortletRequest request) { - return "Handler execution resulted in exception"; - } - - /** * Determine the view name for the given exception, searching the * {@link #setExceptionMappings "exceptionMappings"}, using the diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java new file mode 100644 index 00000000000..bcc5bedb0df --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java @@ -0,0 +1,416 @@ +/* + * Copyright 2002-2009 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.portlet.mvc.annotation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.portlet.ClientDataRequest; +import javax.portlet.Event; +import javax.portlet.EventRequest; +import javax.portlet.MimeResponse; +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.PortletPreferences; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; +import javax.portlet.WindowState; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.ui.Model; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.context.PortletWebRequest; +import org.springframework.web.portlet.handler.AbstractHandlerExceptionResolver; +import org.springframework.web.servlet.View; + +/** + * Implementation of the {@link org.springframework.web.portlet.HandlerExceptionResolver} interface that handles + * exceptions through the {@link ExceptionHandler} annotation. + *

This exception resolver is enabled by default in the {@link org.springframework.web.portlet.DispatcherPortlet}. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver { + + private WebArgumentResolver[] customArgumentResolvers; + + /** + * 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; + } + + @Override + protected ModelAndView doResolveException(PortletRequest request, + MimeResponse response, + Object handler, + Exception ex) { + if (handler != null) { + Method handlerMethod = findBestExceptionHandlerMethod(handler, ex); + if (handlerMethod != null) { + NativeWebRequest webRequest = new PortletWebRequest(request, response); + try { + Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex); + if (logger.isDebugEnabled()) { + logger.debug("Invoking request handler method: " + handlerMethod); + } + Object retVal = doInvokeMethod(handlerMethod, handler, args); + return getModelAndView(retVal); + } + catch (Exception invocationEx) { + logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx); + } + } + } + return null; + } + + /** + * Finds the handler method that matches the thrown exception best. + * + * @param handler the handler object + * @param thrownException the exception to be handled + * @return the best matching method; or null if none is found + */ + private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) { + final Class handlerType = handler.getClass(); + final Class thrownExceptionType = thrownException.getClass(); + + final Map, Method> resolverMethods = + new LinkedHashMap, Method>(); + + ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + method = ClassUtils.getMostSpecificMethod(method, handlerType); + List> handledExceptions = getHandledExceptions(method); + for (Class handledException : handledExceptions) { + if (handledException.isAssignableFrom(thrownExceptionType)) { + if (!resolverMethods.containsKey(handledException)) { + resolverMethods.put(handledException, method); + } + else { + Method oldMappedMethod = resolverMethods.get(handledException); + throw new IllegalStateException( + "Ambiguous exception handler mapped for " + handledException + "]: {" + + oldMappedMethod + ", " + method + "}."); + + } + } + } + } + }); + return getBestMatchingMethod(thrownException, resolverMethods); + } + + /** + * Returns all the exception classes handled by the given method. + * + *

Default implementation looks for exceptions in the {@linkplain ExceptionHandler#value() annotation}, or - + * if that annotation element is empty - any exceptions listed in the method parameters if the method is annotated + * with {@code @ExceptionHandler}. + * + * @param method the method + * @return the handled exceptions + */ + @SuppressWarnings("unchecked") + protected List> getHandledExceptions(Method method) { + List> result = new ArrayList>(); + ExceptionHandler exceptionHandler = method.getAnnotation(ExceptionHandler.class); + if (exceptionHandler != null) { + if (!ObjectUtils.isEmpty(exceptionHandler.value())) { + result.addAll(Arrays.asList(exceptionHandler.value())); + } + else { + for (Class param : method.getParameterTypes()) { + if (Throwable.class.isAssignableFrom(param)) { + result.add((Class) param); + } + } + } + } + return result; + } + + /** + * Returns the best matching method. Uses the {@link DepthComparator}. + */ + private Method getBestMatchingMethod(Exception thrownException, + Map, Method> resolverMethods) { + if (!resolverMethods.isEmpty()) { + List> handledExceptions = + new ArrayList>(resolverMethods.keySet()); + Collections.sort(handledExceptions, new DepthComparator(thrownException)); + Class bestMatchMethod = handledExceptions.get(0); + return resolverMethods.get(bestMatchMethod); + } + else { + return null; + } + } + + /** + * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}. + */ + private Object[] resolveHandlerArguments(Method handlerMethod, + Object handler, + NativeWebRequest webRequest, + Exception thrownException) throws Exception { + + Class[] paramTypes = handlerMethod.getParameterTypes(); + Object[] args = new Object[paramTypes.length]; + + Class handlerType = handler.getClass(); + + for (int i = 0; i < args.length; i++) { + MethodParameter methodParam = new MethodParameter(handlerMethod, i); + GenericTypeResolver.resolveParameterType(methodParam, handlerType); + + Class paramType = methodParam.getParameterType(); + + Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException); + if (argValue != WebArgumentResolver.UNRESOLVED) { + args[i] = argValue; + } + else { + throw new IllegalStateException( + "Unsupported argument [" + paramType.getName() + "] for @ExceptionHandler method: " + + handlerMethod); + } + } + return args; + } + + /** + * Resolves common method arguments. Delegates to registered {@link #setCustomArgumentResolver(org.springframework.web.bind.support.WebArgumentResolver) argumentResolvers} first, + * then checking {@link #resolveStandardArgument}. + * + * @param methodParameter the method parameter + * @param webRequest the request + * @param thrownException the exception thrown + * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED} + */ + protected Object resolveCommonArgument(MethodParameter methodParameter, + NativeWebRequest webRequest, + Exception thrownException) throws Exception { + // Invoke custom argument resolvers if present... + if (this.customArgumentResolvers != null) { + for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) { + Object value = argumentResolver.resolveArgument(methodParameter, webRequest); + if (value != WebArgumentResolver.UNRESOLVED) { + return value; + } + } + } + + // Resolution of standard parameter types... + Class paramType = methodParameter.getParameterType(); + Object value = resolveStandardArgument(paramType, webRequest, thrownException); + if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) { + throw new IllegalStateException( + "Standard argument type [" + paramType.getName() + "] resolved to incompatible value of type [" + + (value != null ? value.getClass() : null) + + "]. Consider declaring the argument type in a less specific fashion."); + } + return value; + } + + /** + * Resolves standard method arguments. Default implementation handles {@link org.springframework.web.context.request.NativeWebRequest}, + * {@link javax.servlet.ServletRequest}, {@link javax.servlet.ServletResponse}, {@link javax.servlet.http.HttpSession}, {@link java.security.Principal}, {@link java.util.Locale}, + * request {@link java.io.InputStream}, request {@link java.io.Reader}, response {@link java.io.OutputStream}, response {@link java.io.Writer}, + * and the given {@code thrownException}. + * + * @param parameterType the method parameter type + * @param webRequest the request + * @param thrownException the exception thrown + * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED} + */ + protected Object resolveStandardArgument(Class parameterType, + NativeWebRequest webRequest, + Exception thrownException) throws Exception { + if (WebRequest.class.isAssignableFrom(parameterType)) { + return webRequest; + } + + PortletRequest request = (PortletRequest) webRequest.getNativeRequest(); + PortletResponse response = (PortletResponse) webRequest.getNativeResponse(); + + if (PortletRequest.class.isAssignableFrom(parameterType)) { + return request; + } + else if (PortletResponse.class.isAssignableFrom(parameterType)) { + return response; + } + else if (PortletSession.class.isAssignableFrom(parameterType)) { + return request.getPortletSession(); + } + else if (PortletPreferences.class.isAssignableFrom(parameterType)) { + return request.getPreferences(); + } + else if (PortletMode.class.isAssignableFrom(parameterType)) { + return request.getPortletMode(); + } + else if (WindowState.class.isAssignableFrom(parameterType)) { + return request.getWindowState(); + } + else if (PortalContext.class.isAssignableFrom(parameterType)) { + return request.getPortalContext(); + } + else if (Principal.class.isAssignableFrom(parameterType)) { + return request.getUserPrincipal(); + } + else if (Locale.class.equals(parameterType)) { + return request.getLocale(); + } + else if (InputStream.class.isAssignableFrom(parameterType)) { + if (!(request instanceof ClientDataRequest)) { + throw new IllegalStateException("InputStream can only get obtained for Action/ResourceRequest"); + } + return ((ClientDataRequest) request).getPortletInputStream(); + } + else if (Reader.class.isAssignableFrom(parameterType)) { + if (!(request instanceof ClientDataRequest)) { + throw new IllegalStateException("Reader can only get obtained for Action/ResourceRequest"); + } + return ((ClientDataRequest) request).getReader(); + } + else if (OutputStream.class.isAssignableFrom(parameterType)) { + if (!(response instanceof MimeResponse)) { + throw new IllegalStateException("OutputStream can only get obtained for Render/ResourceResponse"); + } + return ((MimeResponse) response).getPortletOutputStream(); + } + else if (Writer.class.isAssignableFrom(parameterType)) { + if (!(response instanceof MimeResponse)) { + throw new IllegalStateException("Writer can only get obtained for Render/ResourceResponse"); + } + return ((MimeResponse) response).getWriter(); + } + else if (Event.class.equals(parameterType)) { + if (!(request instanceof EventRequest)) { + throw new IllegalStateException("Event can only get obtained from EventRequest"); + } + return ((EventRequest) request).getEvent(); + } + else if (parameterType.isInstance(thrownException)) { + return thrownException; + } + else { + return WebArgumentResolver.UNRESOLVED; + } + } + + private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception { + ReflectionUtils.makeAccessible(method); + try { + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + ReflectionUtils.rethrowException(ex.getTargetException()); + } + throw new IllegalStateException("Should never get here"); + } + + @SuppressWarnings("unchecked") + private ModelAndView getModelAndView(Object returnValue) { + if (returnValue instanceof ModelAndView) { + return (ModelAndView) returnValue; + } + else if (returnValue instanceof Model) { + return new ModelAndView().addAllObjects(((Model) returnValue).asMap()); + } + else if (returnValue instanceof Map) { + return new ModelAndView().addAllObjects((Map) returnValue); + } + else if (returnValue instanceof View) { + return new ModelAndView(returnValue); + } + else if (returnValue instanceof String) { + return new ModelAndView((String) returnValue); + } + else if (returnValue == null) { + return null; + } + else { + throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); + } + } + + /** + * Comparator capable of sorting exceptions based on their depth from the thrown exception type. + */ + private static class DepthComparator implements Comparator> { + + private final Class handlerExceptionType; + + private DepthComparator(Exception handlerException) { + this.handlerExceptionType = handlerException.getClass(); + } + + public int compare(Class o1, Class o2) { + int depth1 = getDepth(o1, 0); + int depth2 = getDepth(o2, 0); + + return depth2 - depth1; + } + + private int getDepth(Class exceptionType, int depth) { + if (exceptionType.equals(handlerExceptionType)) { + // Found it! + return depth; + } + // If we've gone as far as we can go and haven't found it... + if (Throwable.class.equals(exceptionType)) { + return -1; + } + return getDepth(exceptionType.getSuperclass(), depth + 1); + } + + } +} \ No newline at end of file diff --git a/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java b/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java new file mode 100644 index 00000000000..a9c6066b715 --- /dev/null +++ b/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2009 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.portlet.mvc.annotation; + +import java.io.IOException; +import java.net.BindException; + +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.portlet.MockPortletRequest; +import org.springframework.mock.web.portlet.MockPortletResponse; +import org.springframework.mock.web.portlet.MockRenderRequest; +import org.springframework.mock.web.portlet.MockRenderResponse; +import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.portlet.ModelAndView; + +/** @author Arjen Poutsma */ +public class AnnotationMethodHandlerExceptionResolverTests { + + private AnnotationMethodHandlerExceptionResolver exceptionResolver; + + private MockRenderRequest request; + + private MockRenderResponse response; + + @Before + public void setUp() { + exceptionResolver = new AnnotationMethodHandlerExceptionResolver(); + request = new MockRenderRequest(); + response = new MockRenderResponse(); + } + + @Test + public void simple() { + BindException ex = new BindException(); + SimpleController controller = new SimpleController(); + ModelAndView mav = exceptionResolver.resolveException(request, response, controller, ex); + assertNotNull("No ModelAndView returned", mav); + assertEquals("Invalid view name returned", "BindException", mav.getViewName()); + } + + @Test(expected = IllegalStateException.class) + public void ambiguous() { + IllegalArgumentException ex = new IllegalArgumentException(); + AmbiguousController controller = new AmbiguousController(); + exceptionResolver.resolveException(request, response, controller, ex); + } + + @Controller + private static class SimpleController { + + @ExceptionHandler(IOException.class) + public String handleIOException(IOException ex, PortletRequest request) { + return ClassUtils.getShortName(ex.getClass()); + } + + @ExceptionHandler(BindException.class) + public String handleBindException(Exception ex, PortletResponse response) { + return ClassUtils.getShortName(ex.getClass()); + } + + @ExceptionHandler(IllegalArgumentException.class) + public String handleIllegalArgumentException(Exception ex) { + return ClassUtils.getShortName(ex.getClass()); + } + + } + + @Controller + private static class AmbiguousController { + + @ExceptionHandler({BindException.class, IllegalArgumentException.class}) + public String handle1(Exception ex, PortletRequest request, PortletResponse response) + throws IOException { + return ClassUtils.getShortName(ex.getClass()); + } + + @ExceptionHandler + public String handle2(IllegalArgumentException ex) { + return ClassUtils.getShortName(ex.getClass()); + } + + } +} \ No newline at end of file