View Javadoc

1   package org.springframework.security.util;
2   
3   import java.lang.reflect.InvocationTargetException;
4   import java.util.ArrayList;
5   import java.util.Comparator;
6   import java.util.Iterator;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.TreeMap;
10  
11  /**
12   * Handler for analyzing {@link Throwable} instances.
13   *
14   * Can be subclassed to customize its behavior.
15   * 
16   * @author Andreas Senft
17   * @since 2.0
18   * @version $Id: ThrowableAnalyzer.java 2559 2008-01-30 16:15:02Z luke_t $
19   */
20  public class ThrowableAnalyzer {
21  
22      /**
23       * Default extractor for {@link Throwable} instances.
24       * 
25       * @see Throwable#getCause()
26       */
27      public static final ThrowableCauseExtractor DEFAULT_EXTRACTOR
28          = new ThrowableCauseExtractor() {
29              public Throwable extractCause(Throwable throwable) {
30                  return throwable.getCause();
31              }
32          };
33      
34      /**
35       * Default extractor for {@link InvocationTargetException} instances.
36       * 
37       * @see InvocationTargetException#getTargetException()
38       */
39      public static final ThrowableCauseExtractor INVOCATIONTARGET_EXTRACTOR 
40          = new ThrowableCauseExtractor() {
41              public Throwable extractCause(Throwable throwable) {
42                  verifyThrowableHierarchy(throwable, InvocationTargetException.class);
43                  return ((InvocationTargetException) throwable).getTargetException();
44              }
45          };
46  
47      /**
48       * Comparator to order classes ascending according to their hierarchy relation.
49       * If two classes have a hierarchical relation, the "higher" class is considered 
50       * to be greater by this comparator.<br>
51       * For hierarchically unrelated classes their fully qualified name will be compared. 
52       */
53      private static final Comparator CLASS_HIERARCHY_COMPARATOR = new Comparator() {
54  
55          public int compare(Object o1, Object o2) {
56              Class class1 = (Class) o1;
57              Class class2 = (Class) o2;
58              
59              if (class1.isAssignableFrom(class2)) {
60                  return 1;
61              } else if (class2.isAssignableFrom(class1)) {
62                  return -1;
63              } else {
64                  return class1.getName().compareTo(class2.getName());
65              }
66          }
67          
68      };
69          
70  
71      /**
72       * Map of registered cause extractors.
73       * key: Class<Throwable>; value: ThrowableCauseExctractor
74       */
75      private final Map extractorMap;
76      
77      
78      /**
79       * Creates a new <code>ThrowableAnalyzer</code> instance.
80       */
81      public ThrowableAnalyzer() {
82          this.extractorMap = new TreeMap(CLASS_HIERARCHY_COMPARATOR);
83          
84          initExtractorMap();
85      }
86      
87      /**
88       * Registers a <code>ThrowableCauseExtractor</code> for the specified type.
89       * <i>Can be used in subclasses overriding {@link #initExtractorMap()}.</i>
90       * 
91       * @param throwableType the type (has to be a subclass of <code>Throwable</code>)
92       * @param extractor the associated <code>ThrowableCauseExtractor</code> (not <code>null</code>)
93       * 
94       * @throws IllegalArgumentException if one of the arguments is invalid
95       */
96      protected final void registerExtractor(Class throwableType, ThrowableCauseExtractor extractor) {
97          verifyThrowableType(throwableType);
98  
99          if (extractor == null) {
100             throw new IllegalArgumentException("Invalid extractor: null");
101         }
102 
103         this.extractorMap.put(throwableType, extractor);
104     }
105 
106     /**
107      * Initializes associations between <code>Throwable</code>s and <code>ThrowableCauseExtractor</code>s.
108      * The default implementation performs the following registrations:
109      * <li>{@link #DEFAULT_EXTRACTOR} for {@link Throwable}</li>
110      * <li>{@link #INVOCATIONTARGET_EXTRACTOR} for {@link InvocationTargetException}</li>
111      * <br>
112      * Subclasses overriding this method are encouraged to invoke the super method to perform the
113      * default registrations. They can register additional extractors as required.
114      * <p>
115      * Note: An extractor registered for a specific type is applicable for that type <i>and all subtypes thereof</i>.
116      * However, extractors registered to more specific types are guaranteed to be resolved first.
117      * So in the default case InvocationTargetExceptions will be handled by {@link #INVOCATIONTARGET_EXTRACTOR}
118      * while all other throwables are handled by {@link #DEFAULT_EXTRACTOR}.
119      * 
120      * @see #registerExtractor(Class, ThrowableCauseExtractor)
121      */
122     protected void initExtractorMap() {
123         registerExtractor(InvocationTargetException.class, INVOCATIONTARGET_EXTRACTOR);
124         registerExtractor(Throwable.class, DEFAULT_EXTRACTOR);
125     }
126     
127     /**
128      * Returns an array containing the classes for which extractors are registered.
129      * The order of the classes is the order in which comparisons will occur for
130      * resolving a matching extractor.
131      * 
132      * @return the types for which extractors are registered
133      */
134     final Class[] getRegisteredTypes() {
135         List typeList = new ArrayList(this.extractorMap.keySet());
136         return (Class[]) typeList.toArray(new Class[typeList.size()]);
137     }
138     
139     /**
140      * Determines the cause chain of the provided <code>Throwable</code>.
141      * The returned array contains all throwables extracted from the stacktrace, using the registered
142      * {@link ThrowableCauseExtractor extractors}. The elements of the array are ordered:
143      * The first element is the passed in throwable itself. The following elements
144      * appear in their order downward the stacktrace.
145      * <p>
146      * Note: If no {@link ThrowableCauseExtractor} is registered for this instance 
147      * then the returned array will always only contain the passed in throwable.
148      * 
149      * @param throwable the <code>Throwable</code> to analyze
150      * @return an array of all determined throwables from the stacktrace
151      * 
152      * @throws IllegalArgumentException if the throwable is <code>null</code>
153      * 
154      * @see #initExtractorMap()
155      */
156     public final Throwable[] determineCauseChain(Throwable throwable) {
157         if (throwable == null) {
158             throw new IllegalArgumentException("Invalid throwable: null");
159         }
160         
161         List chain = new ArrayList();
162         Throwable currentThrowable = throwable;
163         
164         while (currentThrowable != null) {
165             chain.add(currentThrowable);
166             currentThrowable = extractCause(currentThrowable);
167         }
168         
169         return (Throwable[]) chain.toArray(new Throwable[chain.size()]);
170     }
171     
172     /**
173      * Extracts the cause of the given throwable using an appropriate extractor.
174      * 
175      * @param throwable the <code>Throwable</code> (not <code>null</code>
176      * @return the cause, may be <code>null</code> if none could be resolved
177      */
178     private Throwable extractCause(Throwable throwable) {
179         for (Iterator iter = this.extractorMap.entrySet().iterator(); iter.hasNext(); ) {
180             Map.Entry entry = (Map.Entry) iter.next();
181             
182             Class throwableType = (Class) entry.getKey();
183             if (throwableType.isInstance(throwable)) {
184                 ThrowableCauseExtractor extractor = (ThrowableCauseExtractor) entry.getValue();
185                 return extractor.extractCause(throwable);
186             }
187         }
188         
189         return null;
190     }
191     
192     /**
193      * Returns the first throwable from the passed in array that is assignable to the provided type.
194      * A returned instance is safe to be cast to the specified type.
195      * <p>
196      * If the passed in array is null or empty this method returns <code>null</code>.
197      * 
198      * @param throwableType the type to look for
199      * @param chain the array (will be processed in element order)
200      * @return the found <code>Throwable</code>, <code>null</code> if not found
201      * 
202      * @throws IllegalArgumentException if the provided type is <code>null</code> 
203      * or no subclass of <code>Throwable</code>
204      */
205     public final Throwable getFirstThrowableOfType(Class throwableType, Throwable[] chain) {
206         verifyThrowableType(throwableType);
207         
208         if (chain != null) {
209             for (int i = 0; i < chain.length; ++i) {
210                 Throwable t = chain[i];
211                 
212                 if ((t != null) && throwableType.isInstance(t)) {
213                     return t;
214                 }
215             }
216         }
217         
218         return null;
219     }
220     
221     /**
222      * Convenience method for verifying that the passed in class refers to a valid 
223      * subclass of <code>Throwable</code>.
224      * 
225      * @param throwableType the type to check
226      * 
227      * @throws IllegalArgumentException if <code>typeToCheck</code> is either <code>null</code>
228      * or not assignable to <code>expectedBaseType</code>
229      */
230     private static void verifyThrowableType(Class throwableType) {
231         if (throwableType == null) {
232             throw new IllegalArgumentException("Invalid type: null");
233         }
234         if (!Throwable.class.isAssignableFrom(throwableType)) {
235             throw new IllegalArgumentException("Invalid type: '" 
236                     + throwableType.getName() 
237                     + "'. Has to be a subclass of '" + Throwable.class.getName() + "'");
238         }
239     }
240     
241     /**
242      * Verifies that the provided throwable is a valid subclass of the provided type (or of the type itself).
243      * If <code>expectdBaseType</code> is <code>null</code>, no check will be performed.
244      * <p>
245      * Can be used for verification purposes in implementations 
246      * of {@link ThrowableCauseExtractor extractors}.
247      * 
248      * @param throwable the <code>Throwable</code> to check
249      * @param expectedBaseType the type to check against
250      * 
251      * @throws IllegalArgumentException if <code>throwable</code> is either <code>null</code>
252      * or its type is not assignable to <code>expectedBaseType</code>
253      */
254     public static final void verifyThrowableHierarchy(Throwable throwable, Class expectedBaseType) {
255         if (expectedBaseType == null) {
256             return;
257         }
258         
259         if (throwable == null) {
260             throw new IllegalArgumentException("Invalid throwable: null");
261         }
262         Class throwableType = throwable.getClass();
263         
264         if (!expectedBaseType.isAssignableFrom(throwableType)) {
265             throw new IllegalArgumentException("Invalid type: '" 
266                     + throwableType.getName() 
267                     + "'. Has to be a subclass of '" + expectedBaseType.getName() + "'");
268         }
269     }
270 }