/**
*
*/
package org.rapidbeans.exception;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Exception that guarantees that you can always deserialize it by changing all
* nested exceptions into instances of this class while storing the original
* instance's class name the original instances properties as string key value
* pairs adds a time stamp information
*
* @author Martin Bluemel
*/
public class UniversalException extends Exception {
/**
* For serialization issues
*/
private static final long serialVersionUID = -4563749306349098771L;
private static final int GET_SEPARATION_INDEX = 3;
private static final String TAB = "\t";
private static final String COLON = ": ";
private static final String AT = "at ";
private Date timeStamp = null;
private String originalClassname;
private final Map<String, String> properties = new HashMap<String, String>();
/**
* Constructor with a message only.
*
* @param message
* the exception message
*/
public UniversalException(final String message) {
super(message);
this.timeStamp = new Date();
}
/**
* Constructor with a cause.
*
* @param cause
* the root cause exception that will be saved as nested
* exception.
*/
public UniversalException(final Throwable cause) {
super("wrapped " + cause.getClass().getName() + COLON + cause.getMessage(), convertCause(cause));
this.timeStamp = new Date();
}
/**
* Constructor with message and cause.
*
* @param message
* the exception message
* @param cause
* the root cause exception that will be saved as nested
* exception.
*/
public UniversalException(final String message, final Throwable cause) {
super(message, convertCause(cause));
this.timeStamp = new Date();
}
/**
* @return the timeStamp
*/
public Date getTimeStamp() {
return (Date) this.timeStamp.clone();
}
/**
* @return the originalClassname
*/
public String getOriginalClassname() {
return originalClassname;
}
/**
* Returns the value of a property with the given name as string.
*
* @param name
* the property's name
*
* @return the value of a property with the given name as string
*/
public String getProperty(final String name) {
return this.properties.get(name);
}
/**
* Convert the complete cause chain into Exceptions if necessary.
*
* @param cause
* the top level cause
*
* @return the top level cause converted into a ServiceException containing
* only ServiceEception as nested exceptions (causes)
*/
private static UniversalException convertCause(final Throwable cause) {
if (cause instanceof UniversalException) {
return (UniversalException) cause;
}
final List<Throwable> causeChain = new ArrayList<Throwable>();
Throwable causeOfCause = cause;
while (causeOfCause != null) {
causeChain.add(causeOfCause);
causeOfCause = causeOfCause.getCause();
}
final int len = causeChain.size();
Throwable lastCause = null;
for (int i = (len - 1); i >= 0; i--) {
final Throwable currentCause = causeChain.get(i);
UniversalException universalException = null;
if (currentCause instanceof UniversalException) {
universalException = (UniversalException) currentCause;
} else {
if (lastCause == null) {
universalException = new UniversalException(currentCause.getMessage());
} else {
universalException = new UniversalException(currentCause.getMessage(), lastCause);
}
universalException.originalClassname = currentCause.getClass().getName();
universalException.setStackTrace(currentCause.getStackTrace());
causeChain.set(i, universalException);
// reflect over the Exception class' getters
for (final Method method : currentCause.getClass().getMethods()) {
if (method.getName().startsWith("get") && method.getName().length() > GET_SEPARATION_INDEX
&& method.getParameterTypes().length == 0
&& Character.isUpperCase(method.getName().charAt(GET_SEPARATION_INDEX))) {
final String key = lowerFirstCharacter(method.getName().substring(3));
// exclude standard Object and Exception getters
if (key.equals("class") || key.equals("cause") || key.equals("message")
|| key.equals("localizedMessage") || key.equals("stackTrace")) {
continue;
}
try {
final Object value = method.invoke(currentCause);
if (value == null) {
universalException.properties.put(key, "<null>");
} else {
universalException.properties.put(key, value.toString());
}
// we can not really reach test coverage for the
// IllegalAccessException
// so in my eyes the IllegalAccesException would
// have been better designed
// as a RntimeException
} catch (IllegalAccessException e) {
putSeviceExceptionProperties(universalException, key);
} catch (InvocationTargetException e) {
putSeviceExceptionProperties(universalException, key);
}
}
}
}
lastCause = universalException;
}
return (UniversalException) causeChain.get(0);
}
/**
* Write an error message into the properties of the given exception.
*
* @param serviceException
* the UniversalException to work on
* @param key
* the property key where we had problems to determine a value
* for.
*/
private static void putSeviceExceptionProperties(UniversalException serviceException, final String key) {
serviceException.properties.put(key, "<problems to retrieve exception property " + key + ">");
}
/**
* Lowers the first character of the given string.
*
* @param s
* the input string
*
* @return the string with lowered first character
*/
private static String lowerFirstCharacter(final String s) {
final String firstCharLowered = s.substring(0, 1).toLowerCase();
if (s.length() == 1) {
return firstCharLowered;
} else {
return firstCharLowered + s.substring(1);
}
}
/**
* Prints the ServiceException and its stack trace to the specified print
* stream.
*
* @param s
* PrintStream to used for output
*/
@Override
public void printStackTrace(final PrintStream s) {
printStackTrace(new OutputMediaAdapterPs(s));
}
/**
* Prints the ServiceException and its stack trace to the specified print
* writer.
*
* @param s
* PrintWriter used for output
*/
@Override
public void printStackTrace(final PrintWriter s) {
printStackTrace(new OutputMediaAdapterPw(s));
}
/**
* Prints the ServiceException and its stack trace to the specified print
* stream.
*
* @param s
* PrintStream to used for output
*/
private void printStackTrace(final OutputMediaAdapter s) {
synchronized (s) {
s.println(this);
s.println(TAB + "time stamp: " + this.timeStamp.toString());
for (final StackTraceElement trace : this.getStackTrace()) {
s.println(TAB + AT + trace);
}
final UniversalException ourCause = (UniversalException) getCause();
if (ourCause != null) {
printStackTraceAsCause(ourCause, s, this.getStackTrace());
}
}
}
/**
* Print the stack trace as a cause for the specified stack trace.
*
* @param t
* the Throwable to print the stack trace for
* @param s
* the stream or write to print to
* @param parentTrace
* the stack trace of the parent exception
*/
private static void printStackTraceAsCause(final UniversalException t, final OutputMediaAdapter s,
final StackTraceElement[] parentTrace) {
// Compute number of frames in common between this and caused
StackTraceElement[] trace = t.getStackTrace();
int m = trace.length - 1;
int n = parentTrace.length - 1;
while (m >= 0 && n >= 0 && trace[m].equals(parentTrace[n])) {
m--;
n--;
}
int framesInCommon = trace.length - 1 - m;
// Print the cause
s.println("Caused by: " + t);
// Print the properties
if (t.properties != null) {
for (final String key : t.properties.keySet()) {
s.println(TAB + key + " = \"" + t.properties.get(key) + "\"");
}
}
// Print the stack trace
for (int i = 0; i <= m; i++) {
s.println(TAB + AT + trace[i]);
}
if (framesInCommon != 0) {
s.println(TAB + "... " + framesInCommon + " more");
}
// Recurse if we have a cause
UniversalException ourCause = (UniversalException) t.getCause();
if (ourCause != null) {
printStackTraceAsCause(ourCause, s, trace);
}
}
/**
* Returns a short description of this throwable. The result is the
* concatenation of:
* <ul>
* <li>the {@linkplain Class#getName() name} of the class of this object
* <li>": " (a colon and a space)
* <li>the result of invoking this object's {@link #getLocalizedMessage} method
* </ul>
* If <tt>getLocalizedMessage</tt> returns <tt>null</tt>, then just the
* class name is returned.
*
* @return a string representation of this throwable.
*/
@Override
public String toString() {
String s = null;
if (this.originalClassname != null) {
s = this.originalClassname;
} else {
s = getClass().getName();
}
final String message = getLocalizedMessage();
return (message != null) ? (s + COLON + message) : s;
}
/**
* The interface a stream or writer class used for stack trace printing has
* to implement.
*
* @author Martin Bluemel
*/
private interface OutputMediaAdapter {
/**
* Prints a line to this stream or writer.
*
* @param o
* the object to print
*/
void println(final Object o);
}
/**
* The print stream implementation of the OutputMediaAdapter.
*
* @author Martin Bluemel
*/
private static class OutputMediaAdapterPs implements OutputMediaAdapter {
private PrintStream ps = null;
/**
* Constructor.
*
* @param stream
* the PrintStream instance to use
*/
public OutputMediaAdapterPs(final PrintStream stream) {
ps = stream;
}
/**
* Delegates the print line request to the PrintStream instance.
*
* @param o
* the Object to print
*/
@Override
public void println(final Object o) {
ps.println(o);
}
}
/**
* The print writer implementation of the OutputMediaAdapter.
*
* @author Martin Bluemel
*/
private static class OutputMediaAdapterPw implements OutputMediaAdapter {
private PrintWriter pw = null;
/**
* Constructor.
*
* @param writer
* the PrintWriter instance to use
*/
public OutputMediaAdapterPw(final PrintWriter writer) {
pw = writer;
}
/**
* Delegates the print line request to the PrintWriter instance.
*
* @param o
* the Object to print
*/
@Override
public void println(final Object o) {
pw.println(o);
}
}
}