/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.commons.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jcoderz.commons.Loggable;
/**
* Helper class around throwables.
* @author Andreas Mandel
*/
public final class ThrowableUtil
{
private static final int MAX_REASONABLE_PARAMETER_LENGTH = 10000;
/** Name of getter methods start with this prefix. */
private static final String GETTER_METHOD_PREFIX = "get";
/** Name of getter methods start with this prefix. */
private static final String BOOLEAN_GETTER_METHOD_PREFIX = "is";
/** Length of the getter prefix. */
private static final int GETTER_METHOD_PREFIX_LENGTH
= GETTER_METHOD_PREFIX.length();
/** Length of the boolean getter prefix. */
private static final int BOOLEAN_GETTER_METHOD_PREFIX_LENGTH
= BOOLEAN_GETTER_METHOD_PREFIX.length();
/**
* Stores the Throwable.getCause() method if this method is available.
* This should be the case for all JDKs > 1.4.
*/
private static final Method GET_CAUSE;
/**
* Stores the Throwable.initCause(Throwable) method if this method
* is available.
* This should be the case for all JDKs > 1.4.
*/
private static final Method INIT_CAUSE;
/** A empty object array. */
private static final Object[] EMPTY_ARRAY = new Object[0];
private static final String CLASSNAME = ThrowableUtil.class.getName();
private static final Logger logger = Logger.getLogger(CLASSNAME);
private static final int MAX_NESTING_DEPTH = 15;
static
{
Method theGetCauseMethod = null;
Method theInitCauseMethod = null;
try
{
theGetCauseMethod
= Throwable.class.getDeclaredMethod("getCause", new Class[0]);
theInitCauseMethod
= Throwable.class.getDeclaredMethod("initCause",
new Class[] {Throwable.class});
}
catch (Exception ex)
{
// Warning, cause this should not fail with JDK > 1.4
logger.log(Level.WARNING, "Could not initialize, will run without.",
ex);
}
GET_CAUSE = theGetCauseMethod;
INIT_CAUSE = theInitCauseMethod;
}
private ThrowableUtil ()
{
// NO Instances
}
/**
* Tries to fix the exception chaining for the given Throwable.
* Some exception classes still use a none standard way
* to nest exceptions. This method tries best to detect this
* classes and pass the nested exceptions into the standard
* nesting mechanism available since JDK1.4 with the throwable
* class. It is save to call this method several times for a
* give exception.
* @param ex the exception to be checked.
*/
public static void fixChaining (Throwable ex)
{
try
{
Throwable current = ex;
int nesting = 0;
while (INIT_CAUSE != null && current != null
&& !(current instanceof Loggable))
{
if (nesting++ > MAX_NESTING_DEPTH)
{
logger.log(Level.FINE,
"Stopped fixing exception nesting cause max depth "
+ "reached for given exception.", ex);
break;
}
if (current.getCause() == null)
{
final Method theGetCauseMethod
= findGetCauseMethod(current.getClass().getMethods());
if (theGetCauseMethod != null)
{
initCause(current, theGetCauseMethod);
}
}
current = current.getCause();
}
}
catch (Exception unexpected)
{
// do not risk any side effect here
logger.log(
Level.SEVERE, "Unexpected exception, ignored.", unexpected);
}
}
/**
* Pull up nested information.
* This method goes down the exception chain of the given
* loggable and if it find getters for properties,
* like <code>getSql()</code> adds the values of these
* properties as parameters to the loggable. The search is
* stopped when either a {@link Loggable} is found in the list,
* the end of the chain is reached or after MAX_NESTING_DEPTH
* steps down the chain.
* @param loggable the Loggable to be feed with parameters.
*/
public static void collectNestedData (Loggable loggable)
{
try
{
Throwable current = loggable.getCause();
int nesting = 0;
while (current != null)
{
if (nesting++ > MAX_NESTING_DEPTH)
{
logger.log(Level.FINE,
"Stopped collecting nested information max depth "
+ "reached for given exception.", loggable);
break;
}
if (!(current instanceof Loggable))
{
collectParameters(loggable, current, nesting);
}
current = current.getCause();
}
}
catch (Exception unexpected)
{
// do not risk any side effect here
logger.log(
Level.SEVERE, "Unexpected exception, ignored.", unexpected);
}
}
/**
* Tries to read additional property like information from this throwable
* and fills it in a map suitable for detailed information output.
* @param thr the throwable to analyze.
* @return a Map pointing from String property names to the value.
*/
public static Map/*<String, Object>*/ getProperties(Throwable thr)
{
final Map/*<String, Object>*/ result = new HashMap();
final Method[] methods = thr.getClass().getMethods();
for (int i = 0; i < methods.length; i++)
{
final int modifier = methods[i].getModifiers();
if (methods[i].getDeclaringClass() != Throwable.class
&& methods[i].getDeclaringClass() != Object.class
&& methods[i].getParameterTypes().length == 0
&& Modifier.isPublic(modifier)
&& !Modifier.isStatic(modifier)
&& !methods[i].getReturnType().equals(Void.TYPE))
{
try
{
if (methods[i].getName().startsWith(GETTER_METHOD_PREFIX))
{
final Object value
= methods[i].invoke(thr, (Object[]) null);
final String key
= methods[i].getName().substring(
GETTER_METHOD_PREFIX_LENGTH);
result.put(key, value);
}
else if (methods[i].getName().startsWith(
BOOLEAN_GETTER_METHOD_PREFIX)
&& (methods[i].getReturnType().equals(Boolean.class)
|| methods[i].getReturnType().equals(
java.lang.Boolean.TYPE)))
{
final Object value
= methods[i].invoke(thr, (Object[]) null);
final String key
= methods[i].getName().substring(
BOOLEAN_GETTER_METHOD_PREFIX_LENGTH);
result.put(key, value);
}
}
catch (InvocationTargetException e)
{
// Ignore this property, continue with next
}
catch (IllegalArgumentException e)
{
// Ignore this property, continue with next
}
catch (IllegalAccessException e)
{
// Ignore this property, continue with next
}
}
}
return result;
}
/**
* Dumps the stack trace of the given throwable to its String representation.
* @param thr the throwable to dump the stack trace from.
* @return a String representation of the given throwable
* @see Throwable#printStackTrace()
*/
public static String toString (Throwable thr)
{
final StringWriter sw = new StringWriter();
PrintWriter pw = null;
try
{
pw = new PrintWriter(sw);
thr.printStackTrace(pw);
}
finally
{
IoUtil.close(pw);
IoUtil.close(sw);
}
return sw.toString();
}
static Method findGetCauseMethod (final Method[] methods)
{
Method theGetCauseMethod = null;
for (int i = 0; i < methods.length; i++)
{
if (methods[i].getDeclaringClass() == Throwable.class)
{
continue;
}
final int modifier = methods[i].getModifiers();
if (methods[i].getParameterTypes().length == 0
&& Modifier.isPublic(modifier)
&& !Modifier.isStatic(modifier)
&& Throwable.class.isAssignableFrom(methods[i].getReturnType()))
{
// if the method is called getCause, assume it does
// what it is named FIXES #76
if (methods[i].getName().equals("getCause"))
{
theGetCauseMethod = methods[i];
break;
}
if (theGetCauseMethod != null)
{
// 2nd hit, safety first
logger.fine("Found 2 matching methods " + theGetCauseMethod
+ " or " + methods[i] + ".");
theGetCauseMethod = null;
break;
}
theGetCauseMethod = methods[i];
}
}
return theGetCauseMethod;
}
private static void initCause (Throwable current, Method theGetCauseMethod)
{
try
{
final Throwable cause
= (Throwable) theGetCauseMethod.invoke(
current, EMPTY_ARRAY);
INIT_CAUSE.invoke(current, new Object[] {cause});
}
catch (Exception e)
{
logger.log(Level.FINEST, "Failed to init cause for " + current
+ " using " + theGetCauseMethod + " got a exception.", e);
}
}
private static void collectParameters (
Loggable loggable, Throwable thr, int nesting)
throws IllegalAccessException, InvocationTargetException
{
final Method[] methods = thr.getClass().getMethods();
for (int i = 0; i < methods.length; i++)
{
final int modifier = methods[i].getModifiers();
if (methods[i].getDeclaringClass() != Throwable.class
&& methods[i].getDeclaringClass() != Object.class
&& methods[i].getParameterTypes().length == 0
&& methods[i].getExceptionTypes().length == 0
&& Modifier.isPublic(modifier)
&& !Modifier.isStatic(modifier)
&& methods[i].getName().startsWith(GETTER_METHOD_PREFIX)
&& !methods[i].getReturnType().equals(Void.TYPE))
{
final Object result = methods[i].invoke(thr, (Object[]) null);
if (result != null && result != thr.getCause())
{
loggable.addParameter(
"CAUSE_" + nesting + "_" + thr.getClass().getName()
+ "#" + methods[i].getName().substring(
GETTER_METHOD_PREFIX_LENGTH),
asString(result));
}
}
}
}
private static String asString (Object obj)
{
String result;
if (obj instanceof Object[])
{
result = ArraysUtil.toString((Object[]) obj);
}
else
{
result = String.valueOf(obj);
}
return StringUtil.trimLength(
result,
MAX_REASONABLE_PARAMETER_LENGTH);
}
}