001/*
002 * Copyright 2002-2015 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.portlet.mvc.annotation;
018
019import java.io.InputStream;
020import java.io.OutputStream;
021import java.io.Reader;
022import java.io.Writer;
023import java.lang.reflect.InvocationTargetException;
024import java.lang.reflect.Method;
025import java.security.Principal;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.concurrent.ConcurrentHashMap;
033import javax.portlet.ClientDataRequest;
034import javax.portlet.Event;
035import javax.portlet.EventRequest;
036import javax.portlet.MimeResponse;
037import javax.portlet.PortalContext;
038import javax.portlet.PortletMode;
039import javax.portlet.PortletPreferences;
040import javax.portlet.PortletRequest;
041import javax.portlet.PortletResponse;
042import javax.portlet.PortletSession;
043import javax.portlet.WindowState;
044import javax.servlet.ServletRequest;
045import javax.servlet.ServletResponse;
046import javax.servlet.http.HttpSession;
047
048import org.springframework.core.ExceptionDepthComparator;
049import org.springframework.core.GenericTypeResolver;
050import org.springframework.core.MethodParameter;
051import org.springframework.core.annotation.AnnotationUtils;
052import org.springframework.core.annotation.SynthesizingMethodParameter;
053import org.springframework.ui.Model;
054import org.springframework.util.ClassUtils;
055import org.springframework.util.ObjectUtils;
056import org.springframework.util.ReflectionUtils;
057import org.springframework.web.bind.annotation.ExceptionHandler;
058import org.springframework.web.bind.support.WebArgumentResolver;
059import org.springframework.web.context.request.NativeWebRequest;
060import org.springframework.web.context.request.WebRequest;
061import org.springframework.web.portlet.ModelAndView;
062import org.springframework.web.portlet.context.PortletWebRequest;
063import org.springframework.web.portlet.handler.AbstractHandlerExceptionResolver;
064import org.springframework.web.servlet.View;
065
066/**
067 * Implementation of the {@link org.springframework.web.portlet.HandlerExceptionResolver} interface that handles
068 * exceptions through the {@link ExceptionHandler} annotation.
069 *
070 * <p>This exception resolver is enabled by default in the {@link org.springframework.web.portlet.DispatcherPortlet}.
071 *
072 * @author Arjen Poutsma
073 * @author Juergen Hoeller
074 * @since 3.0
075 */
076public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
077
078        /**
079         * Arbitrary {@link Method} reference, indicating no method found in the cache.
080         */
081        private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis");
082
083
084        private final Map<Class<?>, Map<Class<? extends Throwable>, Method>> exceptionHandlerCache =
085                        new ConcurrentHashMap<Class<?>, Map<Class<? extends Throwable>, Method>>(64);
086
087        private WebArgumentResolver[] customArgumentResolvers;
088
089
090        /**
091         * Set a custom ArgumentResolvers to use for special method parameter types.
092         * <p>Such a custom ArgumentResolver will kick in first, having a chance to resolve
093         * an argument value before the standard argument handling kicks in.
094         */
095        public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
096                this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
097        }
098
099        /**
100         * Set one or more custom ArgumentResolvers to use for special method parameter types.
101         * <p>Any such custom ArgumentResolver will kick in first, having a chance to resolve
102         * an argument value before the standard argument handling kicks in.
103         */
104        public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
105                this.customArgumentResolvers = argumentResolvers;
106        }
107
108
109        @Override
110        protected ModelAndView doResolveException(
111                        PortletRequest request, MimeResponse response, Object handler, Exception ex) {
112
113                if (handler != null) {
114                        Method handlerMethod = findBestExceptionHandlerMethod(handler, ex);
115                        if (handlerMethod != null) {
116                                NativeWebRequest webRequest = new PortletWebRequest(request, response);
117                                try {
118                                        Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex);
119                                        if (logger.isDebugEnabled()) {
120                                                logger.debug("Invoking request handler method: " + handlerMethod);
121                                        }
122                                        Object retVal = doInvokeMethod(handlerMethod, handler, args);
123                                        return getModelAndView(retVal);
124                                }
125                                catch (Exception invocationEx) {
126                                        logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx);
127                                }
128                        }
129                }
130                return null;
131        }
132
133        /**
134         * Finds the handler method that matches the thrown exception best.
135         * @param handler the handler object
136         * @param thrownException the exception to be handled
137         * @return the best matching method; or {@code null} if none is found
138         */
139        private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) {
140                final Class<?> handlerType = handler.getClass();
141                final Class<? extends Throwable> thrownExceptionType = thrownException.getClass();
142                Method handlerMethod;
143
144                Map<Class<? extends Throwable>, Method> handlers = this.exceptionHandlerCache.get(handlerType);
145                if (handlers != null) {
146                        handlerMethod = handlers.get(thrownExceptionType);
147                        if (handlerMethod != null) {
148                                return (handlerMethod == NO_METHOD_FOUND ? null : handlerMethod);
149                        }
150                }
151                else {
152                        handlers = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
153                        this.exceptionHandlerCache.put(handlerType, handlers);
154                }
155
156                final Map<Class<? extends Throwable>, Method> matchedHandlers = new HashMap<Class<? extends Throwable>, Method>();
157
158                ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() {
159                        @Override
160                        public void doWith(Method method) {
161                                method = ClassUtils.getMostSpecificMethod(method, handlerType);
162                                List<Class<? extends Throwable>> handledExceptions = getHandledExceptions(method);
163                                for (Class<? extends Throwable> handledException : handledExceptions) {
164                                        if (handledException.isAssignableFrom(thrownExceptionType)) {
165                                                if (!matchedHandlers.containsKey(handledException)) {
166                                                        matchedHandlers.put(handledException, method);
167                                                }
168                                                else {
169                                                        Method oldMappedMethod = matchedHandlers.get(handledException);
170                                                        if (!oldMappedMethod.equals(method)) {
171                                                                throw new IllegalStateException(
172                                                                                "Ambiguous exception handler mapped for " + handledException + "]: {" +
173                                                                                                oldMappedMethod + ", " + method + "}.");
174                                                        }
175                                                }
176                                        }
177                                }
178                        }
179                });
180
181                handlerMethod = getBestMatchingMethod(matchedHandlers, thrownException);
182                handlers.put(thrownExceptionType, (handlerMethod == null ? NO_METHOD_FOUND : handlerMethod));
183                return handlerMethod;
184        }
185
186        /**
187         * Returns all the exception classes handled by the given method.
188         * <p>Default implementation looks for exceptions in the {@linkplain ExceptionHandler#value() annotation},
189         * or - if that annotation element is empty - any exceptions listed in the method parameters if the
190         * method is annotated with {@code @ExceptionHandler}.
191         * @param method the method
192         * @return the handled exceptions
193         */
194        @SuppressWarnings("unchecked")
195        protected List<Class<? extends Throwable>> getHandledExceptions(Method method) {
196                List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
197                ExceptionHandler exceptionHandler = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
198                if (exceptionHandler != null) {
199                        if (!ObjectUtils.isEmpty(exceptionHandler.value())) {
200                                result.addAll(Arrays.asList(exceptionHandler.value()));
201                        }
202                        else {
203                                for (Class<?> param : method.getParameterTypes()) {
204                                        if (Throwable.class.isAssignableFrom(param)) {
205                                                result.add((Class<? extends Throwable>) param);
206                                        }
207                                }
208                        }
209                }
210                return result;
211        }
212
213        /**
214         * Uses the {@link ExceptionDepthComparator} to find the best matching method.
215         * @return the best matching method, or {@code null} if none found
216         */
217        private Method getBestMatchingMethod(
218                        Map<Class<? extends Throwable>, Method> resolverMethods, Exception thrownException) {
219
220                if (resolverMethods.isEmpty()) {
221                        return null;
222                }
223                Class<? extends Throwable> closestMatch =
224                                ExceptionDepthComparator.findClosestMatch(resolverMethods.keySet(), thrownException);
225                Method method = resolverMethods.get(closestMatch);
226                return (method == null || NO_METHOD_FOUND == method ? null : method);
227        }
228
229        /**
230         * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}.
231         */
232        private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
233                        NativeWebRequest webRequest, Exception thrownException) throws Exception {
234
235                Class<?>[] paramTypes = handlerMethod.getParameterTypes();
236                Object[] args = new Object[paramTypes.length];
237                Class<?> handlerType = handler.getClass();
238                for (int i = 0; i < args.length; i++) {
239                        MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);
240                        GenericTypeResolver.resolveParameterType(methodParam, handlerType);
241                        Class<?> paramType = methodParam.getParameterType();
242                        Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException);
243                        if (argValue != WebArgumentResolver.UNRESOLVED) {
244                                args[i] = argValue;
245                        }
246                        else {
247                                throw new IllegalStateException("Unsupported argument [" + paramType.getName() +
248                                                "] for @ExceptionHandler method: " + handlerMethod);
249                        }
250                }
251                return args;
252        }
253
254        /**
255         * Resolves common method arguments. Delegates to registered
256         * {@link #setCustomArgumentResolver argumentResolvers} first,
257         * then checking {@link #resolveStandardArgument}.
258         * @param methodParameter the method parameter
259         * @param webRequest the request
260         * @param thrownException the exception thrown
261         * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED}
262         */
263        protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest,
264                        Exception thrownException) throws Exception {
265
266                // Invoke custom argument resolvers if present...
267                if (this.customArgumentResolvers != null) {
268                        for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) {
269                                Object value = argumentResolver.resolveArgument(methodParameter, webRequest);
270                                if (value != WebArgumentResolver.UNRESOLVED) {
271                                        return value;
272                                }
273                        }
274                }
275
276                // Resolution of standard parameter types...
277                Class<?> paramType = methodParameter.getParameterType();
278                Object value = resolveStandardArgument(paramType, webRequest, thrownException);
279                if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) {
280                        throw new IllegalStateException("Standard argument type [" + paramType.getName() +
281                                        "] resolved to incompatible value of type [" + (value != null ? value.getClass() : null) +
282                                        "]. Consider declaring the argument type in a less specific fashion.");
283                }
284                return value;
285        }
286
287        /**
288         * Resolves standard method arguments. The default implementation handles {@link NativeWebRequest},
289         * {@link ServletRequest}, {@link ServletResponse}, {@link HttpSession}, {@link Principal},
290         * {@link Locale}, request {@link InputStream}, request {@link Reader}, response {@link OutputStream},
291         * response {@link Writer}, and the given {@code thrownException}.
292         * @param parameterType the method parameter type
293         * @param webRequest the request
294         * @param thrownException the exception thrown
295         * @return the argument value, or {@link org.springframework.web.bind.support.WebArgumentResolver#UNRESOLVED}
296         */
297        protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest,
298                        Exception thrownException) throws Exception {
299
300                if (parameterType.isInstance(thrownException)) {
301                        return thrownException;
302                }
303                else if (WebRequest.class.isAssignableFrom(parameterType)) {
304                        return webRequest;
305                }
306
307                PortletRequest request = webRequest.getNativeRequest(PortletRequest.class);
308                PortletResponse response = webRequest.getNativeResponse(PortletResponse.class);
309
310                if (PortletRequest.class.isAssignableFrom(parameterType)) {
311                        return request;
312                }
313                else if (PortletResponse.class.isAssignableFrom(parameterType)) {
314                        return response;
315                }
316                else if (PortletSession.class.isAssignableFrom(parameterType)) {
317                        return request.getPortletSession();
318                }
319                else if (PortletPreferences.class.isAssignableFrom(parameterType)) {
320                        return request.getPreferences();
321                }
322                else if (PortletMode.class.isAssignableFrom(parameterType)) {
323                        return request.getPortletMode();
324                }
325                else if (WindowState.class.isAssignableFrom(parameterType)) {
326                        return request.getWindowState();
327                }
328                else if (PortalContext.class.isAssignableFrom(parameterType)) {
329                        return request.getPortalContext();
330                }
331                else if (Principal.class.isAssignableFrom(parameterType)) {
332                        return request.getUserPrincipal();
333                }
334                else if (Locale.class == parameterType) {
335                        return request.getLocale();
336                }
337                else if (InputStream.class.isAssignableFrom(parameterType)) {
338                        if (!(request instanceof ClientDataRequest)) {
339                                throw new IllegalStateException("InputStream can only get obtained for Action/ResourceRequest");
340                        }
341                        return ((ClientDataRequest) request).getPortletInputStream();
342                }
343                else if (Reader.class.isAssignableFrom(parameterType)) {
344                        if (!(request instanceof ClientDataRequest)) {
345                                throw new IllegalStateException("Reader can only get obtained for Action/ResourceRequest");
346                        }
347                        return ((ClientDataRequest) request).getReader();
348                }
349                else if (OutputStream.class.isAssignableFrom(parameterType)) {
350                        if (!(response instanceof MimeResponse)) {
351                                throw new IllegalStateException("OutputStream can only get obtained for Render/ResourceResponse");
352                        }
353                        return ((MimeResponse) response).getPortletOutputStream();
354                }
355                else if (Writer.class.isAssignableFrom(parameterType)) {
356                        if (!(response instanceof MimeResponse)) {
357                                throw new IllegalStateException("Writer can only get obtained for Render/ResourceResponse");
358                        }
359                        return ((MimeResponse) response).getWriter();
360                }
361                else if (Event.class == parameterType) {
362                        if (!(request instanceof EventRequest)) {
363                                throw new IllegalStateException("Event can only get obtained from EventRequest");
364                        }
365                        return ((EventRequest) request).getEvent();
366                }
367                else {
368                        return WebArgumentResolver.UNRESOLVED;
369                }
370        }
371
372        private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception {
373                ReflectionUtils.makeAccessible(method);
374                try {
375                        return method.invoke(target, args);
376                }
377                catch (InvocationTargetException ex) {
378                        ReflectionUtils.rethrowException(ex.getTargetException());
379                }
380                throw new IllegalStateException("Should never get here");
381        }
382
383        @SuppressWarnings("unchecked")
384        private ModelAndView getModelAndView(Object returnValue) {
385                if (returnValue instanceof ModelAndView) {
386                        return (ModelAndView) returnValue;
387                }
388                else if (returnValue instanceof Model) {
389                        return new ModelAndView().addAllObjects(((Model) returnValue).asMap());
390                }
391                else if (returnValue instanceof Map) {
392                        return new ModelAndView().addAllObjects((Map<String, Object>) returnValue);
393                }
394                else if (returnValue instanceof View) {
395                        return new ModelAndView(returnValue);
396                }
397                else if (returnValue instanceof String) {
398                        return new ModelAndView((String) returnValue);
399                }
400                else if (returnValue == null) {
401                        return new ModelAndView();
402                }
403                else {
404                        throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
405                }
406        }
407
408}