View Javadoc

1   /*
2    * This file is part of Domingo
3    * an Open Source Java-API to Lotus Notes/Domino
4    * hosted at http://domingo.sourceforge.net
5    *
6    * Copyright (c) 2003-2007 Beck et al. projects GmbH Munich, Germany (http://www.bea.de)
7    *
8    * This library is free software; you can redistribute it and/or
9    * modify it under the terms of the GNU Lesser General Public
10   * License as published by the Free Software Foundation; either
11   * version 2.1 of the License, or (at your option) any later version.
12   *
13   * This library is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   * Lesser General Public License for more details.
17   *
18   * You should have received a copy of the GNU Lesser General Public
19   * License along with this library; if not, write to the Free Software
20   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
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>'&#x2e;' == {@value}</code>. */
61      public static final char PACKAGE_SEPARATOR_CHAR = '.';
62  
63      /*** The package separator String: <code>"&#x2e;"</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             // exception ignored
182         } catch (SecurityException ignored) {
183             // exception ignored
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                 // exception ignored
191             } catch (IllegalArgumentException ignored) {
192                 // exception ignored
193             } catch (InvocationTargetException ignored) {
194                 // exception ignored
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             // exception ignored
213         } catch (SecurityException ignored) {
214             // exception ignored
215         }
216 
217         if (field != null && Throwable.class.isAssignableFrom(field.getType())) {
218             try {
219                 return (Throwable) field.get(throwable);
220             } catch (IllegalAccessException ignored) {
221                 // exception ignored
222             } catch (IllegalArgumentException ignored) {
223                 // exception ignored
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                 // exception ignored
274             } catch (SecurityException ignored) {
275                 // exception ignored
276             }
277         }
278 
279         try {
280             Field field = cls.getField("detail");
281             if (field != null) {
282                 return true;
283             }
284         } catch (NoSuchFieldException ignored) {
285             // exception ignored
286         } catch (SecurityException ignored) {
287             // exception ignored
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             // Remove the frame from the cause trace if it is the same
309             // as in the wrapper trace
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>&quot;&nbsp;&nbsp;&nbsp;at&quot;.</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             // Determine if the line starts with <whitespace>at
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) {  // handle inner classes
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         // if running on jre1.4 or higher, use default printStackTrace
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         // generating the cascading stack trace
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         // Avoid infinite loop between decompose() and printStackTrace().
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 }