001/**
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017package org.apache.isis.core.commons.exceptions;
018
019
020import java.io.PrintWriter;
021import java.io.StringWriter;
022import java.lang.reflect.Field;
023import java.lang.reflect.InvocationTargetException;
024import java.lang.reflect.Method;
025import java.sql.SQLException;
026import java.util.ArrayList;
027import java.util.List;
028
029/**
030 * <p>Provides utilities for manipulating and examining 
031 * <code>Throwable</code> objects.</p>
032 *
033 * @author Daniel L. Rall
034 * @author Dmitri Plotnikov
035 * @author Stephen Colebourne
036 * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
037 * @author Pete Gieser
038 * @since 1.0
039 * @version $Id: ExceptionUtils.java 594278 2007-11-12 19:58:30Z bayard $
040 */
041// portions copied in from commons-lang 2.6
042public class ExceptionUtils {
043    
044    /**
045     * <p>The names of methods commonly used to access a wrapped exception.</p>
046     */
047    private static String[] CAUSE_METHOD_NAMES = {
048        "getCause",
049        "getNextException",
050        "getTargetException",
051        "getException",
052        "getSourceException",
053        "getRootCause",
054        "getCausedByException",
055        "getNested",
056        "getLinkedException",
057        "getNestedException",
058        "getLinkedCause",
059        "getThrowable",
060    };
061
062    /**
063     * <p>The Method object for Java 1.4 getCause.</p>
064     */
065    private static final Method THROWABLE_CAUSE_METHOD;
066
067    /**
068     * <p>The Method object for Java 1.4 initCause.</p>
069     */
070    private static final Method THROWABLE_INITCAUSE_METHOD;
071    
072    static {
073        Method causeMethod;
074        try {
075            causeMethod = Throwable.class.getMethod("getCause", null);
076        } catch (Exception e) {
077            causeMethod = null;
078        }
079        THROWABLE_CAUSE_METHOD = causeMethod;
080        try {
081            causeMethod = Throwable.class.getMethod("initCause", new Class[]{Throwable.class});
082        } catch (Exception e) {
083            causeMethod = null;
084        }
085        THROWABLE_INITCAUSE_METHOD = causeMethod;
086    }
087    
088    /**
089     * <p>
090     * Public constructor allows an instance of <code>ExceptionUtils</code> to be created, although that is not
091     * normally necessary.
092     * </p>
093     */
094    public ExceptionUtils() {
095        super();
096    }
097
098    //-----------------------------------------------------------------------
099    /**
100     * <p>Introspects the <code>Throwable</code> to obtain the cause.</p>
101     *
102     * <p>The method searches for methods with specific names that return a 
103     * <code>Throwable</code> object. This will pick up most wrapping exceptions,
104     * including those from JDK 1.4, and
105     * {@link org.apache.commons.lang.exception.NestableException NestableException}.
106     * The method names can be added to using {@link #addCauseMethodName(String)}.</p>
107     *
108     * <p>The default list searched for are:</p>
109     * <ul>
110     *  <li><code>getCause()</code></li>
111     *  <li><code>getNextException()</code></li>
112     *  <li><code>getTargetException()</code></li>
113     *  <li><code>getException()</code></li>
114     *  <li><code>getSourceException()</code></li>
115     *  <li><code>getRootCause()</code></li>
116     *  <li><code>getCausedByException()</code></li>
117     *  <li><code>getNested()</code></li>
118     * </ul>
119     * 
120     * <p>In the absence of any such method, the object is inspected for a
121     * <code>detail</code> field assignable to a <code>Throwable</code>.</p>
122     *
123     * <p>If none of the above is found, returns <code>null</code>.</p>
124     *
125     * @param throwable  the throwable to introspect for a cause, may be null
126     * @return the cause of the <code>Throwable</code>,
127     *  <code>null</code> if none found or null throwable input
128     * @since 1.0
129     */
130    public static Throwable getCause(Throwable throwable) {
131        synchronized(CAUSE_METHOD_NAMES) {
132            return getCause(throwable, CAUSE_METHOD_NAMES);
133        }
134    }
135
136    /**
137     * <p>Introspects the <code>Throwable</code> to obtain the cause.</p>
138     *
139     * <ol>
140     * <li>Try known exception types.</li>
141     * <li>Try the supplied array of method names.</li>
142     * <li>Try the field 'detail'.</li>
143     * </ol>
144     *
145     * <p>A <code>null</code> set of method names means use the default set.
146     * A <code>null</code> in the set of method names will be ignored.</p>
147     *
148     * @param throwable  the throwable to introspect for a cause, may be null
149     * @param methodNames  the method names, null treated as default set
150     * @return the cause of the <code>Throwable</code>,
151     *  <code>null</code> if none found or null throwable input
152     * @since 1.0
153     */
154    public static Throwable getCause(Throwable throwable, String[] methodNames) {
155        if (throwable == null) {
156            return null;
157        }
158        Throwable cause = getCauseUsingWellKnownTypes(throwable);
159        if (cause == null) {
160            if (methodNames == null) {
161                synchronized(CAUSE_METHOD_NAMES) {
162                    methodNames = CAUSE_METHOD_NAMES;
163                }
164            }
165            for (int i = 0; i < methodNames.length; i++) {
166                String methodName = methodNames[i];
167                if (methodName != null) {
168                    cause = getCauseUsingMethodName(throwable, methodName);
169                    if (cause != null) {
170                        break;
171                    }
172                }
173            }
174
175            if (cause == null) {
176                cause = getCauseUsingFieldName(throwable, "detail");
177            }
178        }
179        return cause;
180    }
181
182    /**
183     * <p>Finds a <code>Throwable</code> for known types.</p>
184     * 
185     * <p>Uses <code>instanceof</code> checks to examine the exception,
186     * looking for well known types which could contain chained or
187     * wrapped exceptions.</p>
188     *
189     * @param throwable  the exception to examine
190     * @return the wrapped exception, or <code>null</code> if not found
191     */
192    private static Throwable getCauseUsingWellKnownTypes(Throwable throwable) {
193        /*if (throwable instanceof Nestable) {
194            return ((Nestable) throwable).getCause();
195        } else */
196        if (throwable instanceof SQLException) {
197            return ((SQLException) throwable).getNextException();
198        } else if (throwable instanceof InvocationTargetException) {
199            return ((InvocationTargetException) throwable).getTargetException();
200        } else {
201            return null;
202        }
203    }
204
205    /**
206     * <p>Finds a <code>Throwable</code> by method name.</p>
207     *
208     * @param throwable  the exception to examine
209     * @param methodName  the name of the method to find and invoke
210     * @return the wrapped exception, or <code>null</code> if not found
211     */
212    private static Throwable getCauseUsingMethodName(Throwable throwable, String methodName) {
213        Method method = null;
214        try {
215            method = throwable.getClass().getMethod(methodName, null);
216        } catch (NoSuchMethodException ignored) {
217            // exception ignored
218        } catch (SecurityException ignored) {
219            // exception ignored
220        }
221
222        if (method != null && Throwable.class.isAssignableFrom(method.getReturnType())) {
223            try {
224                return (Throwable) method.invoke(throwable, /*ArrayUtils.*/EMPTY_OBJECT_ARRAY);
225            } catch (IllegalAccessException ignored) {
226                // exception ignored
227            } catch (IllegalArgumentException ignored) {
228                // exception ignored
229            } catch (InvocationTargetException ignored) {
230                // exception ignored
231            }
232        }
233        return null;
234    }
235    
236    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
237
238    /**
239     * <p>Finds a <code>Throwable</code> by field name.</p>
240     *
241     * @param throwable  the exception to examine
242     * @param fieldName  the name of the attribute to examine
243     * @return the wrapped exception, or <code>null</code> if not found
244     */
245    private static Throwable getCauseUsingFieldName(Throwable throwable, String fieldName) {
246        Field field = null;
247        try {
248            field = throwable.getClass().getField(fieldName);
249        } catch (NoSuchFieldException ignored) {
250            // exception ignored
251        } catch (SecurityException ignored) {
252            // exception ignored
253        }
254
255        if (field != null && Throwable.class.isAssignableFrom(field.getType())) {
256            try {
257                return (Throwable) field.get(throwable);
258            } catch (IllegalAccessException ignored) {
259                // exception ignored
260            } catch (IllegalArgumentException ignored) {
261                // exception ignored
262            }
263        }
264        return null;
265    }
266
267    //-----------------------------------------------------------------------
268    /**
269     * <p>Checks if the Throwable class has a <code>getCause</code> method.</p>
270     *
271     * <p>This is true for JDK 1.4 and above.</p>
272     *
273     * @return true if Throwable is nestable
274     * @since 2.0
275     */
276    public static boolean isThrowableNested() {
277        return THROWABLE_CAUSE_METHOD != null;
278    }
279    
280    /**
281     * <p>Checks whether this <code>Throwable</code> class can store a cause.</p>
282     *
283     * <p>This method does <b>not</b> check whether it actually does store a cause.<p>
284     *
285     * @param throwable  the <code>Throwable</code> to examine, may be null
286     * @return boolean <code>true</code> if nested otherwise <code>false</code>
287     * @since 2.0
288     */
289    public static boolean isNestedThrowable(Throwable throwable) {
290        if (throwable == null) {
291            return false;
292        }
293
294        /*if (throwable instanceof Nestable) {
295            return true;
296        } else*/ if (throwable instanceof SQLException) {
297            return true;
298        } else if (throwable instanceof InvocationTargetException) {
299            return true;
300        } else if (isThrowableNested()) {
301            return true;
302        }
303
304        Class cls = throwable.getClass();
305        synchronized(CAUSE_METHOD_NAMES) {
306            for (int i = 0, isize = CAUSE_METHOD_NAMES.length; i < isize; i++) {
307                try {
308                    Method method = cls.getMethod(CAUSE_METHOD_NAMES[i], null);
309                    if (method != null && Throwable.class.isAssignableFrom(method.getReturnType())) {
310                        return true;
311                    }
312                } catch (NoSuchMethodException ignored) {
313                    // exception ignored
314                } catch (SecurityException ignored) {
315                    // exception ignored
316                }
317            }
318        }
319
320        try {
321            Field field = cls.getField("detail");
322            if (field != null) {
323                return true;
324            }
325        } catch (NoSuchFieldException ignored) {
326            // exception ignored
327        } catch (SecurityException ignored) {
328            // exception ignored
329        }
330
331        return false;
332    }
333
334    /**
335     * <p>Returns the list of <code>Throwable</code> objects in the
336     * exception chain.</p>
337     *
338     * <p>A throwable without cause will return an array containing
339     * one element - the input throwable.
340     * A throwable with one cause will return an array containing
341     * two elements. - the input throwable and the cause throwable.
342     * A <code>null</code> throwable will return an array of size zero.</p>
343     *
344     * <p>From version 2.2, this method handles recursive cause structures
345     * that might otherwise cause infinite loops. The cause chain is
346     * processed until the end is reached, or until the next item in the
347     * chain is already in the result set.</p>
348     *
349     * @see #getThrowableList(Throwable)
350     * @param throwable  the throwable to inspect, may be null
351     * @return the array of throwables, never null
352     */
353    public static Throwable[] getThrowables(Throwable throwable) {
354        List list = getThrowableList(throwable);
355        return (Throwable[]) list.toArray(new Throwable[list.size()]);
356    }
357
358    /**
359     * <p>Returns the list of <code>Throwable</code> objects in the
360     * exception chain.</p>
361     *
362     * <p>A throwable without cause will return a list containing
363     * one element - the input throwable.
364     * A throwable with one cause will return a list containing
365     * two elements. - the input throwable and the cause throwable.
366     * A <code>null</code> throwable will return a list of size zero.</p>
367     *
368     * <p>This method handles recursive cause structures that might
369     * otherwise cause infinite loops. The cause chain is processed until
370     * the end is reached, or until the next item in the chain is already
371     * in the result set.</p>
372     *
373     * @param throwable  the throwable to inspect, may be null
374     * @return the list of throwables, never null
375     * @since Commons Lang 2.2
376     */
377    public static List getThrowableList(Throwable throwable) {
378        List list = new ArrayList();
379        while (throwable != null && list.contains(throwable) == false) {
380            list.add(throwable);
381            throwable = ExceptionUtils.getCause(throwable);
382        }
383        return list;
384    }
385
386    //-----------------------------------------------------------------------
387    /**
388     * <p>A way to get the entire nested stack-trace of an throwable.</p>
389     *
390     * <p>The result of this method is highly dependent on the JDK version
391     * and whether the exceptions override printStackTrace or not.</p>
392     *
393     * @param throwable  the <code>Throwable</code> to be examined
394     * @return the nested stack trace, with the root cause first
395     * @since 2.0
396     */
397    public static String getFullStackTrace(Throwable throwable) {
398        StringWriter sw = new StringWriter();
399        PrintWriter pw = new PrintWriter(sw, true);
400        Throwable[] ts = getThrowables(throwable);
401        for (int i = 0; i < ts.length; i++) {
402            ts[i].printStackTrace(pw);
403            if (isNestedThrowable(ts[i])) {
404                break;
405            }
406        }
407        return sw.getBuffer().toString();
408    }
409
410}