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.servlet.mvc.annotation;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.io.Reader;
023import java.io.Writer;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.security.Principal;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Locale;
033import java.util.Map;
034import java.util.concurrent.ConcurrentHashMap;
035import javax.servlet.ServletException;
036import javax.servlet.ServletRequest;
037import javax.servlet.ServletResponse;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040import javax.servlet.http.HttpSession;
041import javax.xml.transform.Source;
042
043import org.springframework.core.ExceptionDepthComparator;
044import org.springframework.core.GenericTypeResolver;
045import org.springframework.core.MethodParameter;
046import org.springframework.core.annotation.AnnotatedElementUtils;
047import org.springframework.core.annotation.AnnotationUtils;
048import org.springframework.core.annotation.SynthesizingMethodParameter;
049import org.springframework.http.HttpInputMessage;
050import org.springframework.http.HttpOutputMessage;
051import org.springframework.http.HttpStatus;
052import org.springframework.http.MediaType;
053import org.springframework.http.converter.ByteArrayHttpMessageConverter;
054import org.springframework.http.converter.HttpMessageConverter;
055import org.springframework.http.converter.StringHttpMessageConverter;
056import org.springframework.http.converter.xml.SourceHttpMessageConverter;
057import org.springframework.http.server.ServletServerHttpRequest;
058import org.springframework.http.server.ServletServerHttpResponse;
059import org.springframework.ui.Model;
060import org.springframework.util.ClassUtils;
061import org.springframework.util.ObjectUtils;
062import org.springframework.util.ReflectionUtils;
063import org.springframework.util.StringUtils;
064import org.springframework.web.bind.annotation.ExceptionHandler;
065import org.springframework.web.bind.annotation.ResponseBody;
066import org.springframework.web.bind.annotation.ResponseStatus;
067import org.springframework.web.bind.support.WebArgumentResolver;
068import org.springframework.web.context.request.NativeWebRequest;
069import org.springframework.web.context.request.ServletWebRequest;
070import org.springframework.web.context.request.WebRequest;
071import org.springframework.web.servlet.ModelAndView;
072import org.springframework.web.servlet.View;
073import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
074import org.springframework.web.servlet.support.RequestContextUtils;
075
076/**
077 * Implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver} interface that handles
078 * exceptions through the {@link ExceptionHandler} annotation.
079 *
080 * <p>This exception resolver is enabled by default in the {@link org.springframework.web.servlet.DispatcherServlet}.
081 *
082 * @author Arjen Poutsma
083 * @author Juergen Hoeller
084 * @since 3.0
085 * @deprecated as of Spring 3.2, in favor of
086 * {@link org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver ExceptionHandlerExceptionResolver}
087 */
088@Deprecated
089public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
090
091        /**
092         * Arbitrary {@link Method} reference, indicating no method found in the cache.
093         */
094        private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis");
095
096
097        private final Map<Class<?>, Map<Class<? extends Throwable>, Method>> exceptionHandlerCache =
098                        new ConcurrentHashMap<Class<?>, Map<Class<? extends Throwable>, Method>>(64);
099
100        private WebArgumentResolver[] customArgumentResolvers;
101
102        private HttpMessageConverter<?>[] messageConverters =
103                        new HttpMessageConverter<?>[] {new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
104                        new SourceHttpMessageConverter<Source>(),
105                        new org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter()};
106
107
108        /**
109         * Set a custom ArgumentResolvers to use for special method parameter types.
110         * <p>Such a custom ArgumentResolver will kick in first, having a chance to resolve
111         * an argument value before the standard argument handling kicks in.
112         */
113        public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
114                this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
115        }
116
117        /**
118         * Set one or more custom ArgumentResolvers to use for special method parameter types.
119         * <p>Any such custom ArgumentResolver will kick in first, having a chance to resolve
120         * an argument value before the standard argument handling kicks in.
121         */
122        public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
123                this.customArgumentResolvers = argumentResolvers;
124        }
125
126        /**
127         * Set the message body converters to use.
128         * <p>These converters are used to convert from and to HTTP requests and responses.
129         */
130        public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
131                this.messageConverters = messageConverters;
132        }
133
134
135        @Override
136        protected ModelAndView doResolveException(
137                        HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
138
139                if (handler != null) {
140                        Method handlerMethod = findBestExceptionHandlerMethod(handler, ex);
141                        if (handlerMethod != null) {
142                                ServletWebRequest webRequest = new ServletWebRequest(request, response);
143                                try {
144                                        Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex);
145                                        if (logger.isDebugEnabled()) {
146                                                logger.debug("Invoking request handler method: " + handlerMethod);
147                                        }
148                                        Object retVal = doInvokeMethod(handlerMethod, handler, args);
149                                        return getModelAndView(handlerMethod, retVal, webRequest);
150                                }
151                                catch (Exception invocationEx) {
152                                        logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx);
153                                }
154                        }
155                }
156                return null;
157        }
158
159        /**
160         * Finds the handler method that matches the thrown exception best.
161         * @param handler the handler object
162         * @param thrownException the exception to be handled
163         * @return the best matching method; or {@code null} if none is found
164         */
165        private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) {
166                final Class<?> handlerType = ClassUtils.getUserClass(handler);
167                final Class<? extends Throwable> thrownExceptionType = thrownException.getClass();
168                Method handlerMethod = null;
169
170                Map<Class<? extends Throwable>, Method> handlers = this.exceptionHandlerCache.get(handlerType);
171                if (handlers != null) {
172                        handlerMethod = handlers.get(thrownExceptionType);
173                        if (handlerMethod != null) {
174                                return (handlerMethod == NO_METHOD_FOUND ? null : handlerMethod);
175                        }
176                }
177                else {
178                        handlers = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
179                        this.exceptionHandlerCache.put(handlerType, handlers);
180                }
181
182                final Map<Class<? extends Throwable>, Method> matchedHandlers = new HashMap<Class<? extends Throwable>, Method>();
183
184                ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() {
185                        @Override
186                        public void doWith(Method method) {
187                                method = ClassUtils.getMostSpecificMethod(method, handlerType);
188                                List<Class<? extends Throwable>> handledExceptions = getHandledExceptions(method);
189                                for (Class<? extends Throwable> handledException : handledExceptions) {
190                                        if (handledException.isAssignableFrom(thrownExceptionType)) {
191                                                if (!matchedHandlers.containsKey(handledException)) {
192                                                        matchedHandlers.put(handledException, method);
193                                                }
194                                                else {
195                                                        Method oldMappedMethod = matchedHandlers.get(handledException);
196                                                        if (!oldMappedMethod.equals(method)) {
197                                                                throw new IllegalStateException(
198                                                                                "Ambiguous exception handler mapped for " + handledException + "]: {" +
199                                                                                                oldMappedMethod + ", " + method + "}.");
200                                                        }
201                                                }
202                                        }
203                                }
204                        }
205                });
206
207                handlerMethod = getBestMatchingMethod(matchedHandlers, thrownException);
208                handlers.put(thrownExceptionType, (handlerMethod == null ? NO_METHOD_FOUND : handlerMethod));
209                return handlerMethod;
210        }
211
212        /**
213         * Returns all the exception classes handled by the given method.
214         * <p>The default implementation looks for exceptions in the annotation,
215         * or - if that annotation element is empty - any exceptions listed in the method parameters if the method
216         * is annotated with {@code @ExceptionHandler}.
217         * @param method the method
218         * @return the handled exceptions
219         */
220        @SuppressWarnings("unchecked")
221        protected List<Class<? extends Throwable>> getHandledExceptions(Method method) {
222                List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
223                ExceptionHandler exceptionHandler = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
224                if (exceptionHandler != null) {
225                        if (!ObjectUtils.isEmpty(exceptionHandler.value())) {
226                                result.addAll(Arrays.asList(exceptionHandler.value()));
227                        }
228                        else {
229                                for (Class<?> param : method.getParameterTypes()) {
230                                        if (Throwable.class.isAssignableFrom(param)) {
231                                                result.add((Class<? extends Throwable>) param);
232                                        }
233                                }
234                        }
235                }
236                return result;
237        }
238
239        /**
240         * Uses the {@link ExceptionDepthComparator} to find the best matching method.
241         * @return the best matching method, or {@code null} if none found
242         */
243        private Method getBestMatchingMethod(
244                        Map<Class<? extends Throwable>, Method> resolverMethods, Exception thrownException) {
245
246                if (resolverMethods.isEmpty()) {
247                        return null;
248                }
249                Class<? extends Throwable> closestMatch =
250                                ExceptionDepthComparator.findClosestMatch(resolverMethods.keySet(), thrownException);
251                Method method = resolverMethods.get(closestMatch);
252                return ((method == null) || (NO_METHOD_FOUND == method)) ? null : method;
253        }
254
255        /**
256         * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}.
257         */
258        private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
259                        NativeWebRequest webRequest, Exception thrownException) throws Exception {
260
261                Class<?>[] paramTypes = handlerMethod.getParameterTypes();
262                Object[] args = new Object[paramTypes.length];
263                Class<?> handlerType = handler.getClass();
264                for (int i = 0; i < args.length; i++) {
265                        MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);
266                        GenericTypeResolver.resolveParameterType(methodParam, handlerType);
267                        Class<?> paramType = methodParam.getParameterType();
268                        Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException);
269                        if (argValue != WebArgumentResolver.UNRESOLVED) {
270                                args[i] = argValue;
271                        }
272                        else {
273                                throw new IllegalStateException("Unsupported argument [" + paramType.getName() +
274                                                "] for @ExceptionHandler method: " + handlerMethod);
275                        }
276                }
277                return args;
278        }
279
280        /**
281         * Resolves common method arguments. Delegates to registered {@link #setCustomArgumentResolver(WebArgumentResolver)
282         * argumentResolvers} first, then checking {@link #resolveStandardArgument}.
283         * @param methodParameter the method parameter
284         * @param webRequest the request
285         * @param thrownException the exception thrown
286         * @return the argument value, or {@link WebArgumentResolver#UNRESOLVED}
287         */
288        protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest,
289                        Exception thrownException) throws Exception {
290
291                // Invoke custom argument resolvers if present...
292                if (this.customArgumentResolvers != null) {
293                        for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) {
294                                Object value = argumentResolver.resolveArgument(methodParameter, webRequest);
295                                if (value != WebArgumentResolver.UNRESOLVED) {
296                                        return value;
297                                }
298                        }
299                }
300
301                // Resolution of standard parameter types...
302                Class<?> paramType = methodParameter.getParameterType();
303                Object value = resolveStandardArgument(paramType, webRequest, thrownException);
304                if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) {
305                        throw new IllegalStateException(
306                                        "Standard argument type [" + paramType.getName() + "] resolved to incompatible value of type [" +
307                                                        (value != null ? value.getClass() : null) +
308                                                        "]. Consider declaring the argument type in a less specific fashion.");
309                }
310                return value;
311        }
312
313        /**
314         * Resolves standard method arguments. The default implementation handles {@link NativeWebRequest},
315         * {@link ServletRequest}, {@link ServletResponse}, {@link HttpSession}, {@link Principal},
316         * {@link Locale}, request {@link InputStream}, request {@link Reader}, response {@link OutputStream},
317         * response {@link Writer}, and the given {@code thrownException}.
318         * @param parameterType the method parameter type
319         * @param webRequest the request
320         * @param thrownException the exception thrown
321         * @return the argument value, or {@link WebArgumentResolver#UNRESOLVED}
322         */
323        protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest,
324                        Exception thrownException) throws Exception {
325
326                if (parameterType.isInstance(thrownException)) {
327                        return thrownException;
328                }
329                else if (WebRequest.class.isAssignableFrom(parameterType)) {
330                        return webRequest;
331                }
332
333                HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
334                HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
335
336                if (ServletRequest.class.isAssignableFrom(parameterType)) {
337                        return request;
338                }
339                else if (ServletResponse.class.isAssignableFrom(parameterType)) {
340                        return response;
341                }
342                else if (HttpSession.class.isAssignableFrom(parameterType)) {
343                        return request.getSession();
344                }
345                else if (Principal.class.isAssignableFrom(parameterType)) {
346                        return request.getUserPrincipal();
347                }
348                else if (Locale.class == parameterType) {
349                        return RequestContextUtils.getLocale(request);
350                }
351                else if (InputStream.class.isAssignableFrom(parameterType)) {
352                        return request.getInputStream();
353                }
354                else if (Reader.class.isAssignableFrom(parameterType)) {
355                        return request.getReader();
356                }
357                else if (OutputStream.class.isAssignableFrom(parameterType)) {
358                        return response.getOutputStream();
359                }
360                else if (Writer.class.isAssignableFrom(parameterType)) {
361                        return response.getWriter();
362                }
363                else {
364                        return WebArgumentResolver.UNRESOLVED;
365
366                }
367        }
368
369        private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception {
370                ReflectionUtils.makeAccessible(method);
371                try {
372                        return method.invoke(target, args);
373                }
374                catch (InvocationTargetException ex) {
375                        ReflectionUtils.rethrowException(ex.getTargetException());
376                }
377                throw new IllegalStateException("Should never get here");
378        }
379
380        @SuppressWarnings("unchecked")
381        private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest)
382                        throws Exception {
383
384                ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
385                if (responseStatus != null) {
386                        HttpStatus statusCode = responseStatus.code();
387                        String reason = responseStatus.reason();
388                        if (!StringUtils.hasText(reason)) {
389                                webRequest.getResponse().setStatus(statusCode.value());
390                        }
391                        else {
392                                webRequest.getResponse().sendError(statusCode.value(), reason);
393                        }
394                }
395
396                if (returnValue != null && AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) {
397                        return handleResponseBody(returnValue, webRequest);
398                }
399
400                if (returnValue instanceof ModelAndView) {
401                        return (ModelAndView) returnValue;
402                }
403                else if (returnValue instanceof Model) {
404                        return new ModelAndView().addAllObjects(((Model) returnValue).asMap());
405                }
406                else if (returnValue instanceof Map) {
407                        return new ModelAndView().addAllObjects((Map<String, Object>) returnValue);
408                }
409                else if (returnValue instanceof View) {
410                        return new ModelAndView((View) returnValue);
411                }
412                else if (returnValue instanceof String) {
413                        return new ModelAndView((String) returnValue);
414                }
415                else if (returnValue == null) {
416                        return new ModelAndView();
417                }
418                else {
419                        throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
420                }
421        }
422
423        @SuppressWarnings({ "unchecked", "rawtypes", "resource" })
424        private ModelAndView handleResponseBody(Object returnValue, ServletWebRequest webRequest)
425                        throws ServletException, IOException {
426
427                HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest());
428                List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
429                if (acceptedMediaTypes.isEmpty()) {
430                        acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
431                }
432                MediaType.sortByQualityValue(acceptedMediaTypes);
433                HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
434                Class<?> returnValueType = returnValue.getClass();
435                if (this.messageConverters != null) {
436                        for (MediaType acceptedMediaType : acceptedMediaTypes) {
437                                for (HttpMessageConverter messageConverter : this.messageConverters) {
438                                        if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
439                                                messageConverter.write(returnValue, acceptedMediaType, outputMessage);
440                                                return new ModelAndView();
441                                        }
442                                }
443                        }
444                }
445                if (logger.isWarnEnabled()) {
446                        logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " +
447                                        acceptedMediaTypes);
448                }
449                return null;
450        }
451
452}