1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 package de.bea.domingo.exception;
24
25 import java.io.PrintStream;
26 import java.io.PrintWriter;
27 import java.io.StringWriter;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.sql.SQLException;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.StringTokenizer;
37
38 /***
39 * Utilities for manipulating and examining <code>Throwable</code>s.
40 */
41 public final class ExceptionUtil {
42
43 /*** Platform specific line separator. */
44 private static final String LINE_SEPARATOR;
45
46 /*** An empty immutable <code>String</code> array. */
47 private static final String[] EMPTY_STRING_ARRAY = new String[0];
48
49 /*** An empty immutable <code>Object</code> array. */
50 private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
51
52 /*** Names of methods commonly used to access a wrapped exception. */
53 private static final String[] CAUSE_METHOD_NAMES = { "getCause", "getNextException", "getTargetException", "getException",
54 "getSourceException", "getRootCause", "getCausedByException", "getNested", "getLinkedException",
55 "getNestedException", "getLinkedCause", "getThrowable", };
56
57 /*** Method object for Java 1.4 getCause. */
58 private static final Method THROWABLE_CAUSE_METHOD;
59
60 /*** Package separator character: <code>'.' == {@value}</code>. */
61 public static final char PACKAGE_SEPARATOR_CHAR = '.';
62
63 /*** The package separator String: <code>"."</code>. */
64 public static final String PACKAGE_SEPARATOR = String.valueOf(PACKAGE_SEPARATOR_CHAR);
65
66 /*** Inner class separator character: <code>'$' == {@value}</code>. */
67 public static final char INNER_CLASS_SEPARATOR_CHAR = '$';
68
69 static {
70 Method causeMethod;
71 try {
72 causeMethod = Throwable.class.getMethod("getCause", null);
73 } catch (Exception e) {
74 causeMethod = null;
75 }
76 THROWABLE_CAUSE_METHOD = causeMethod;
77 String lineSeperator = null;
78 try {
79 lineSeperator = System.getProperty("line.separator");
80 } catch (SecurityException ex) {
81 }
82 LINE_SEPARATOR = lineSeperator == null ? "\n" : lineSeperator;
83 }
84
85 /***
86 * Prevents instantiation.
87 */
88 private ExceptionUtil() {
89 super();
90 }
91
92 /***
93 * Returns the given list as a <code>String[]</code>.
94 *
95 * @param list a list to transform.
96 * @return the given list as a <code>String[]</code>.
97 */
98 private static String[] toArray(final List list) {
99 return (String[]) list.toArray(new String[list.size()]);
100 }
101
102 /***
103 * <p>Introspects the <code>Throwable</code> to obtain the cause.</p>
104 *
105 * <p>The method searches for methods with specific names that return a
106 * <code>Throwable</code> object. This will pick up most wrapping
107 * exceptions, including those from JDK 1.4, and the domingo exceptions.</p>
108 *
109 * <p>The default list searched for are:</p> <ul> <li><code>getCause()</code></li>
110 * <li><code>getNextException()</code></li> <li><code>getTargetException()</code></li>
111 * <li><code>getException()</code></li> <li><code>getSourceException()</code></li>
112 * <li><code>getRootCause()</code></li> <li><code>getCausedByException()</code></li>
113 * <li><code>getNested()</code></li> </ul>
114 *
115 * <p>In the absence of any such method, the object is inspected for a
116 * <code>detail</code> field assignable to a <code>Throwable</code>.</p>
117 *
118 * <p>If none of the above is found, returns <code>null</code>.</p>
119 *
120 * @param throwable the throwable to introspect for a cause, may be null
121 * @return the cause of the <code>Throwable</code>, <code>null</code>
122 * if none found or null throwable input
123 */
124 public static Throwable getCause(final Throwable throwable) {
125 if (throwable == null) {
126 return null;
127 }
128 Throwable cause = getCauseUsingWellKnownTypes(throwable);
129 if (cause == null) {
130 for (int i = 0; i < CAUSE_METHOD_NAMES.length; i++) {
131 String methodName = CAUSE_METHOD_NAMES[i];
132 if (methodName != null) {
133 cause = getCauseUsingMethodName(throwable, methodName);
134 if (cause != null) {
135 break;
136 }
137 }
138 }
139
140 if (cause == null) {
141 cause = getCauseUsingFieldName(throwable, "detail");
142 }
143 }
144 return cause;
145 }
146
147 /***
148 * <p>Finds a <code>Throwable</code> for known types.</p>
149 *
150 * <p>Uses <code>instanceof</code> checks to examine the exception,
151 * looking for well known types which could contain chained or wrapped
152 * exceptions.</p>
153 *
154 * @param throwable the exception to examine
155 * @return the wrapped exception, or <code>null</code> if not found
156 */
157 private static Throwable getCauseUsingWellKnownTypes(final Throwable throwable) {
158 if (throwable instanceof CascadingThrowable) {
159 return ((CascadingThrowable) throwable).getCause();
160 } else if (throwable instanceof SQLException) {
161 return ((SQLException) throwable).getNextException();
162 } else if (throwable instanceof InvocationTargetException) {
163 return ((InvocationTargetException) throwable).getTargetException();
164 } else {
165 return null;
166 }
167 }
168
169 /***
170 * <p>Finds a <code>Throwable</code> by method name.</p>
171 *
172 * @param throwable the exception to examine
173 * @param methodName the name of the method to find and invoke
174 * @return the wrapped exception, or <code>null</code> if not found
175 */
176 private static Throwable getCauseUsingMethodName(final Throwable throwable, final String methodName) {
177 Method method = null;
178 try {
179 method = throwable.getClass().getMethod(methodName, null);
180 } catch (NoSuchMethodException ignored) {
181
182 } catch (SecurityException ignored) {
183
184 }
185
186 if (method != null && Throwable.class.isAssignableFrom(method.getReturnType())) {
187 try {
188 return (Throwable) method.invoke(throwable, EMPTY_OBJECT_ARRAY);
189 } catch (IllegalAccessException ignored) {
190
191 } catch (IllegalArgumentException ignored) {
192
193 } catch (InvocationTargetException ignored) {
194
195 }
196 }
197 return null;
198 }
199
200 /***
201 * <p>Finds a <code>Throwable</code> by field name.</p>
202 *
203 * @param throwable the exception to examine
204 * @param fieldName the name of the attribute to examine
205 * @return the wrapped exception, or <code>null</code> if not found
206 */
207 private static Throwable getCauseUsingFieldName(final Throwable throwable, final String fieldName) {
208 Field field = null;
209 try {
210 field = throwable.getClass().getField(fieldName);
211 } catch (NoSuchFieldException ignored) {
212
213 } catch (SecurityException ignored) {
214
215 }
216
217 if (field != null && Throwable.class.isAssignableFrom(field.getType())) {
218 try {
219 return (Throwable) field.get(throwable);
220 } catch (IllegalAccessException ignored) {
221
222 } catch (IllegalArgumentException ignored) {
223
224 }
225 }
226 return null;
227 }
228
229 /***
230 * <p>Checks if the Throwable class has a <code>getCause</code> method.</p>
231 *
232 * <p>This is true for JDK 1.4 and above.</p>
233 *
234 * @return true if Throwable is cascading
235 */
236 public static boolean isCascadingThrowable() {
237 return THROWABLE_CAUSE_METHOD != null;
238 }
239
240 /***
241 * <p>Checks whether this <code>Throwable</code> class can store a cause.</p>
242 *
243 * <p>This method does <b>not</b> check whether it actually does store a
244 * cause.<p>
245 *
246 * @param throwable the <code>Throwable</code> to examine, may be null
247 * @return boolean <code>true</code> if cascading, otherwise <code>false</code>
248 */
249 public static boolean isCascadingThrowable(final Throwable throwable) {
250 if (throwable == null) {
251 return false;
252 }
253
254 if (throwable instanceof CascadingThrowable) {
255 return true;
256 } else if (throwable instanceof SQLException) {
257 return true;
258 } else if (throwable instanceof InvocationTargetException) {
259 return true;
260 } else if (isCascadingThrowable()) {
261 return true;
262 }
263
264 Class cls = throwable.getClass();
265 int isize = CAUSE_METHOD_NAMES.length;
266 for (int i = 0; i < isize; i++) {
267 try {
268 Method method = cls.getMethod(CAUSE_METHOD_NAMES[i], null);
269 if (method != null && Throwable.class.isAssignableFrom(method.getReturnType())) {
270 return true;
271 }
272 } catch (NoSuchMethodException ignored) {
273
274 } catch (SecurityException ignored) {
275
276 }
277 }
278
279 try {
280 Field field = cls.getField("detail");
281 if (field != null) {
282 return true;
283 }
284 } catch (NoSuchFieldException ignored) {
285
286 } catch (SecurityException ignored) {
287
288 }
289
290 return false;
291 }
292
293 /***
294 * <p>Removes common frames from the cause trace given the two stack
295 * traces.</p>
296 *
297 * @param causeFrames stack trace of a cause throwable
298 * @param wrapperFrames stack trace of a wrapper throwable
299 * @throws IllegalArgumentException if either argument is null
300 */
301 public static void removeCommonFrames(final List causeFrames, final List wrapperFrames) throws IllegalArgumentException {
302 if (causeFrames == null || wrapperFrames == null) {
303 throw new IllegalArgumentException("The List must not be null");
304 }
305 int causeFrameIndex = causeFrames.size() - 1;
306 int wrapperFrameIndex = wrapperFrames.size() - 1;
307 while (causeFrameIndex >= 0 && wrapperFrameIndex >= 0) {
308
309
310 String causeFrame = (String) causeFrames.get(causeFrameIndex);
311 String wrapperFrame = (String) wrapperFrames.get(wrapperFrameIndex);
312 if (causeFrame.equals(wrapperFrame)) {
313 causeFrames.remove(causeFrameIndex);
314 }
315 causeFrameIndex--;
316 wrapperFrameIndex--;
317 }
318 }
319
320 /***
321 * <p>Gets the stack trace from a Throwable as a String.</p>
322 *
323 * <p>The result of this method vary by JDK version as this method uses
324 * {@link Throwable#printStackTrace(java.io.PrintWriter)}. On JDK1.3 and
325 * earlier, the cause exception will not be shown unless the specified
326 * throwable alters printStackTrace.</p>
327 *
328 * @param throwable the <code>Throwable</code> to be examined
329 * @return the stack trace as generated by the exception's
330 * <code>printStackTrace(PrintWriter)</code> method
331 */
332 public static String getStackTrace(final Throwable throwable) {
333 StringWriter sw = new StringWriter();
334 PrintWriter pw = new PrintWriter(sw, true);
335 throwable.printStackTrace(pw);
336 return sw.getBuffer().toString();
337 }
338
339 /***
340 * <p>Returns an array where each element is a line from the argument.</p>
341 *
342 * <p>The end of line is determined by the value of
343 * <tt>line.separator</tt> system property.</p>
344 *
345 * @param stackTrace a stack trace String
346 * @return an array where each element is a line from the argument
347 */
348 static String[] getStackFrames(final String stackTrace) {
349 String linebreak = LINE_SEPARATOR;
350 StringTokenizer frames = new StringTokenizer(stackTrace, linebreak);
351 List list = new ArrayList();
352 while (frames.hasMoreTokens()) {
353 list.add(frames.nextToken());
354 }
355 return toArray(list);
356 }
357
358 /***
359 * <p>Produces a <code>List</code> of stack frames - the message is not
360 * included. Only the trace of the specified exception is returned, any
361 * caused by trace is stripped.</p>
362 *
363 * <p>This works in most cases - it will only fail if the exception message
364 * contains a line that starts with:
365 * <code>" at".</code></p>
366 *
367 * @param t is any throwable
368 * @return List of stack frames
369 */
370 static List getStackFrameList(final Throwable t) {
371 String stackTrace = getStackTrace(t);
372 String linebreak = LINE_SEPARATOR;
373 StringTokenizer frames = new StringTokenizer(stackTrace, linebreak);
374 List list = new ArrayList();
375 boolean traceStarted = false;
376 while (frames.hasMoreTokens()) {
377 String token = frames.nextToken();
378
379 int at = token.indexOf("at");
380 if (at != -1 && token.substring(0, at).trim().length() == 0) {
381 traceStarted = true;
382 list.add(token);
383 } else if (traceStarted) {
384 break;
385 }
386 }
387 return list;
388 }
389
390 /***
391 * <p>Gets the class name minus the package name for an <code>Object</code>.</p>
392 *
393 * @param object the class to get the short name for, may be null
394 * @param valueIfNull the value to return if null
395 * @return the class name of the object without the package name, or the null value
396 */
397 public static String getShortClassName(final Object object, final String valueIfNull) {
398 if (object == null) {
399 return valueIfNull;
400 }
401 return getShortClassName(object.getClass().getName());
402 }
403
404 /***
405 * <p>Gets the class name minus the package name from a <code>Class</code>.</p>
406 *
407 * @param clazz the class to get the short name for.
408 * @return the class name without the package name or an empty string
409 */
410 public static String getShortClassName(final Class clazz) {
411 if (clazz == null) {
412 return "";
413 }
414 return getShortClassName(clazz.getName());
415 }
416
417 /***
418 * <p>Gets the class name minus the package name from a String.</p>
419 *
420 * <p>The string passed in is assumed to be a class name - it is not checked.</p>
421 *
422 * @param className the className to get the short name for
423 * @return the class name of the class without the package name or an empty string
424 */
425 public static String getShortClassName(final String className) {
426 if (className == null) {
427 return "";
428 }
429 if (className.length() == 0) {
430 return "";
431 }
432 char[] chars = className.toCharArray();
433 int lastDot = 0;
434 for (int i = 0; i < chars.length; i++) {
435 if (chars[i] == PACKAGE_SEPARATOR_CHAR) {
436 lastDot = i + 1;
437 } else if (chars[i] == INNER_CLASS_SEPARATOR_CHAR) {
438 chars[i] = PACKAGE_SEPARATOR_CHAR;
439 }
440 }
441 return new String(chars, lastDot, chars.length - lastDot);
442 }
443
444 /***
445 * Prints the stack trace of a throwable to the standard error stream.
446 *
447 * @param throwable a cascading throwable
448 */
449 public static void printStackTrace(final Throwable throwable) {
450 printStackTrace(throwable, System.err);
451 }
452
453 /***
454 * Prints the stack trace of a throwable to the specified stream.
455 *
456 * @param throwable a cascading throwable
457 * @param out <code>PrintStream</code> to use for output.
458 * @see #printStackTrace(Throwable, PrintWriter)
459 */
460 public static void printStackTrace(final Throwable throwable, final PrintStream out) {
461 synchronized (out) {
462 PrintWriter pw = new PrintWriter(out, false);
463 printStackTrace(throwable, pw);
464 pw.flush();
465 }
466 }
467
468 /***
469 * Prints the stack trace of a throwable to the specified writer.
470 *
471 * @param throwable a cascading throwable
472 * @param out <code>PrintWriter</code> to use for output.
473 */
474 public static void printStackTrace(final Throwable throwable, final PrintWriter out) {
475 Throwable currentThrowable = throwable;
476
477 if (ExceptionUtil.isCascadingThrowable()) {
478 if (currentThrowable instanceof CascadingThrowable) {
479 ((CascadingThrowable) currentThrowable).printPartialStackTrace(out);
480 } else {
481 currentThrowable.printStackTrace(out);
482 }
483 return;
484 }
485
486
487 List stacks = new ArrayList();
488 while (currentThrowable != null) {
489 String[] st = getStackFrames(currentThrowable);
490 stacks.add(st);
491 currentThrowable = ExceptionUtil.getCause(currentThrowable);
492 }
493
494 String separatorLine = "Caused by: ";
495 trimStackFrames(stacks);
496
497 synchronized (out) {
498 for (Iterator iter = stacks.iterator(); iter.hasNext();) {
499 String[] st = (String[]) iter.next();
500 int len = st.length;
501 for (int i = 0; i < len; i++) {
502 out.println(st[i]);
503 }
504 if (iter.hasNext()) {
505 out.print(separatorLine);
506 }
507 }
508 }
509 }
510
511 /***
512 * Captures the stack trace associated with the specified
513 * <code>Throwable</code> object, decomposing it into a list of stack
514 * frames.
515 *
516 * @param throwable The <code>Throwable</code>.
517 * @return An array of strings describing each stack frame.
518 */
519 protected static String[] getStackFrames(final Throwable throwable) {
520 if (throwable == null) {
521 return EMPTY_STRING_ARRAY;
522 }
523 StringWriter sw = new StringWriter();
524 PrintWriter pw = new PrintWriter(sw, true);
525
526
527 if (throwable instanceof CascadingThrowable) {
528 ((CascadingThrowable) throwable).printPartialStackTrace(pw);
529 } else {
530 throwable.printStackTrace(pw);
531 }
532 return ExceptionUtil.getStackFrames(sw.getBuffer().toString());
533 }
534
535 /***
536 * Trims the stack frames. The first set is left untouched. The rest of the
537 * frames are truncated from the bottom by comparing with one just on top.
538 *
539 * @param stacks The list containing String[] elements
540 */
541 protected static void trimStackFrames(final List stacks) {
542 int size = stacks.size();
543 for (int i = size - 1; i > 0; i--) {
544 String[] curr = (String[]) stacks.get(i);
545 String[] next = (String[]) stacks.get(i - 1);
546
547 List currList = new ArrayList(Arrays.asList(curr));
548 List nextList = new ArrayList(Arrays.asList(next));
549 ExceptionUtil.removeCommonFrames(currList, nextList);
550
551 int trimmed = curr.length - currList.size();
552 if (trimmed > 0) {
553 currList.add("\t... " + trimmed + " more");
554 stacks.set(i, currList.toArray(new String[currList.size()]));
555 }
556 }
557 }
558 }