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 }