001/*
002 * Copyright 2002-2019 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.messaging.handler.invocation;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.springframework.core.ExceptionDepthComparator;
026import org.springframework.lang.Nullable;
027import org.springframework.util.Assert;
028import org.springframework.util.ConcurrentReferenceHashMap;
029
030/**
031 * Cache exception handling method mappings and provide options to look up a method
032 * that should handle an exception. If multiple methods match, they are sorted using
033 * {@link ExceptionDepthComparator} and the top match is returned.
034 *
035 * @author Rossen Stoyanchev
036 * @author Juergen Hoeller
037 * @since 4.0
038 */
039public abstract class AbstractExceptionHandlerMethodResolver {
040
041        private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
042
043        private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
044
045
046        /**
047         * Protected constructor accepting exception-to-method mappings.
048         */
049        protected AbstractExceptionHandlerMethodResolver(Map<Class<? extends Throwable>, Method> mappedMethods) {
050                Assert.notNull(mappedMethods, "Mapped Methods must not be null");
051                this.mappedMethods.putAll(mappedMethods);
052        }
053
054        /**
055         * Extract the exceptions this method handles.This implementation looks for
056         * sub-classes of Throwable in the method signature.
057         * The method is static to ensure safe use from sub-class constructors.
058         */
059        @SuppressWarnings("unchecked")
060        protected static List<Class<? extends Throwable>> getExceptionsFromMethodSignature(Method method) {
061                List<Class<? extends Throwable>> result = new ArrayList<>();
062                for (Class<?> paramType : method.getParameterTypes()) {
063                        if (Throwable.class.isAssignableFrom(paramType)) {
064                                result.add((Class<? extends Throwable>) paramType);
065                        }
066                }
067                if (result.isEmpty()) {
068                        throw new IllegalStateException("No exception types mapped to " + method);
069                }
070                return result;
071        }
072
073
074        /**
075         * Whether the contained type has any exception mappings.
076         */
077        public boolean hasExceptionMappings() {
078                return !this.mappedMethods.isEmpty();
079        }
080
081        /**
082         * Find a {@link Method} to handle the given exception.
083         * Use {@link ExceptionDepthComparator} if more than one match is found.
084         * @param exception the exception
085         * @return a Method to handle the exception, or {@code null} if none found
086         */
087        @Nullable
088        public Method resolveMethod(Throwable exception) {
089                Method method = resolveMethodByExceptionType(exception.getClass());
090                if (method == null) {
091                        Throwable cause = exception.getCause();
092                        if (cause != null) {
093                                method = resolveMethodByExceptionType(cause.getClass());
094                        }
095                }
096                return method;
097        }
098
099        /**
100         * Find a {@link Method} to handle the given exception type. This can be
101         * useful if an {@link Exception} instance is not available (e.g. for tools).
102         * @param exceptionType the exception type
103         * @return a Method to handle the exception, or {@code null} if none found
104         * @since 4.3.1
105         */
106        @Nullable
107        public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
108                Method method = this.exceptionLookupCache.get(exceptionType);
109                if (method == null) {
110                        method = getMappedMethod(exceptionType);
111                        this.exceptionLookupCache.put(exceptionType, method);
112                }
113                return method;
114        }
115
116        /**
117         * Return the {@link Method} mapped to the given exception type, or {@code null} if none.
118         */
119        @Nullable
120        private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
121                List<Class<? extends Throwable>> matches = new ArrayList<>();
122                for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
123                        if (mappedException.isAssignableFrom(exceptionType)) {
124                                matches.add(mappedException);
125                        }
126                }
127                if (!matches.isEmpty()) {
128                        matches.sort(new ExceptionDepthComparator(exceptionType));
129                        return this.mappedMethods.get(matches.get(0));
130                }
131                else {
132                        return null;
133                }
134        }
135
136}