1 /*
2  * Copyright 2013-2019 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16
17 package org.springframework.retry.annotation;
18
19 import java.lang.reflect.Method;
20 import java.util.HashMap;
21 import java.util.Map;
22
23 import org.springframework.classify.SubclassClassifier;
24 import org.springframework.core.annotation.AnnotationUtils;
25 import org.springframework.retry.ExhaustedRetryException;
26 import org.springframework.retry.interceptor.MethodInvocationRecoverer;
27 import org.springframework.util.ReflectionUtils;
28 import org.springframework.util.ReflectionUtils.MethodCallback;
29
30 /**
31  * A recoverer for method invocations based on the <code>@Recover</code> annotation. A
32  * suitable recovery method is one with a Throwable type as the first parameter and the
33  * same return type and arguments as the method that failed. The Throwable first argument
34  * is optional and if omitted the method is treated as a default (called when there are no
35  * other matches). Generally the best matching method is chosen based on the type of the
36  * first parameter and the type of the exception being handled. The closest match in the
37  * class hierarchy is chosen, so for instance if an IllegalArgumentException is being
38  * handled and there is a method whose first argument is RuntimeException, then it will be
39  * preferred over a method whose first argument is Throwable.
40  *
41  * @author Dave Syer
42  * @author Josh Long
43  * @author Aldo Sinanaj
44  * @author Randell Callahan
45  * @author NathanaĆ«l Roberts
46  * @param <T> the type of the return value from the recovery
47  */

48 public class RecoverAnnotationRecoveryHandler<T> implements MethodInvocationRecoverer<T> {
49
50     private SubclassClassifier<Throwable, Method> classifier = new SubclassClassifier<Throwable, Method>();
51
52     private Map<Method, SimpleMetadata> methods = new HashMap<Method, SimpleMetadata>();
53
54     private Object target;
55
56     public RecoverAnnotationRecoveryHandler(Object target, Method method) {
57         this.target = target;
58         init(target, method);
59     }
60
61     @Override
62     public T recover(Object[] args, Throwable cause) {
63         Method method = findClosestMatch(args, cause.getClass());
64         if (method == null) {
65             throw new ExhaustedRetryException("Cannot locate recovery method", cause);
66         }
67         SimpleMetadata meta = this.methods.get(method);
68         Object[] argsToUse = meta.getArgs(cause, args);
69         boolean methodAccessible = method.isAccessible();
70         try {
71             ReflectionUtils.makeAccessible(method);
72             @SuppressWarnings("unchecked")
73             T result = (T) ReflectionUtils.invokeMethod(method, this.target, argsToUse);
74             return result;
75         }
76         finally {
77             if (methodAccessible != method.isAccessible()) {
78                 method.setAccessible(methodAccessible);
79             }
80         }
81     }
82
83     private Method findClosestMatch(Object[] args, Class<? extends Throwable> cause) {
84         int min = Integer.MAX_VALUE;
85         Method result = null;
86         for (Method method : this.methods.keySet()) {
87             SimpleMetadata meta = this.methods.get(method);
88             Class<? extends Throwable> type = meta.getType();
89             if (type == null) {
90                 type = Throwable.class;
91             }
92             if (type.isAssignableFrom(cause)) {
93                 int distance = calculateDistance(cause, type);
94                 if (distance < min) {
95                     min = distance;
96                     result = method;
97                 }
98                 else if (distance == min) {
99                     boolean parametersMatch = compareParameters(args, meta.getArgCount(),
100                             method.getParameterTypes());
101                     if (parametersMatch) {
102                         result = method;
103                     }
104                 }
105             }
106         }
107         return result;
108     }
109
110     private int calculateDistance(Class<? extends Throwable> cause,
111             Class<? extends Throwable> type) {
112         int result = 0;
113         Class<?> current = cause;
114         while (current != type && current != Throwable.class) {
115             result++;
116             current = current.getSuperclass();
117         }
118         return result;
119     }
120
121     private boolean compareParameters(Object[] args, int argCount,
122             Class<?>[] parameterTypes) {
123         if (argCount == (args.length + 1)) {
124             int startingIndex = 0;
125             if (parameterTypes.length > 0
126                     && Throwable.class.isAssignableFrom(parameterTypes[0])) {
127                 startingIndex = 1;
128             }
129             for (int i = startingIndex; i < parameterTypes.length; i++) {
130                 final Object argument = i - startingIndex < args.length
131                         ? args[i - startingIndex] : null;
132                 if (argument == null) {
133                     continue;
134                 }
135                 if (!parameterTypes[i].isAssignableFrom(argument.getClass())) {
136                     return false;
137                 }
138             }
139             return true;
140         }
141         return false;
142     }
143
144     private void init(Object target, Method method) {
145         final Map<Class<? extends Throwable>, Method> types = new HashMap<Class<? extends Throwable>, Method>();
146         final Method failingMethod = method;
147         ReflectionUtils.doWithMethods(failingMethod.getDeclaringClass(),
148                 new MethodCallback() {
149                     @Override
150                     public void doWith(Method method)
151                             throws IllegalArgumentException, IllegalAccessException {
152                         Recover recover = AnnotationUtils.findAnnotation(method,
153                                 Recover.class);
154                         if (recover != null && method.getReturnType()
155                                 .isAssignableFrom(failingMethod.getReturnType())) {
156                             Class<?>[] parameterTypes = method.getParameterTypes();
157                             if (parameterTypes.length > 0 && Throwable.class
158                                     .isAssignableFrom(parameterTypes[0])) {
159                                 @SuppressWarnings("unchecked")
160                                 Class<? extends Throwable> type = (Class<? extends Throwable>) parameterTypes[0];
161                                 types.put(type, method);
162                                 RecoverAnnotationRecoveryHandler.this.methods.put(method,
163                                         new SimpleMetadata(parameterTypes.length, type));
164                             }
165                             else {
166                                 RecoverAnnotationRecoveryHandler.this.classifier
167                                         .setDefaultValue(method);
168                                 RecoverAnnotationRecoveryHandler.this.methods.put(method,
169                                         new SimpleMetadata(parameterTypes.length, null));
170                             }
171                         }
172                     }
173                 });
174         this.classifier.setTypeMap(types);
175         optionallyFilterMethodsBy(failingMethod.getReturnType());
176     }
177
178     private void optionallyFilterMethodsBy(Class<?> returnClass) {
179         Map<Method, SimpleMetadata> filteredMethods = new HashMap<Method, SimpleMetadata>();
180         for (Method method : this.methods.keySet()) {
181             if (method.getReturnType() == returnClass) {
182                 filteredMethods.put(method, this.methods.get(method));
183             }
184         }
185         if (filteredMethods.size() > 0) {
186             this.methods = filteredMethods;
187         }
188     }
189
190     private static class SimpleMetadata {
191
192         private int argCount;
193
194         private Class<? extends Throwable> type;
195
196         public SimpleMetadata(int argCount, Class<? extends Throwable> type) {
197             super();
198             this.argCount = argCount;
199             this.type = type;
200         }
201
202         public int getArgCount() {
203             return this.argCount;
204         }
205
206         public Class<? extends Throwable> getType() {
207             return this.type;
208         }
209
210         public Object[] getArgs(Throwable t, Object[] args) {
211             Object[] result = new Object[getArgCount()];
212             int startArgs = 0;
213             if (this.type != null) {
214                 result[0] = t;
215                 startArgs = 1;
216             }
217             int length = result.length - startArgs > args.length ? args.length
218                     : result.length - startArgs;
219             if (length == 0) {
220                 return result;
221             }
222             System.arraycopy(args, 0, result, startArgs, length);
223             return result;
224         }
225
226     }
227
228 }
229