blob: eccd8b9d9c510e48e65380a44e20ecf896253d73 [file] [log] [blame]
// Copyright 2004, 2005 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.util.exception;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Analyzes an exception, creating one or more {@link ExceptionDescription}s from it.
*
* @author Howard Lewis Ship
*/
public class ExceptionAnalyzer
{
private static final int SKIP_LEADING_WHITESPACE = 0;
private static final int SKIP_T = 1;
private static final int SKIP_OTHER_WHITESPACE = 2;
private final List exceptionDescriptions = new ArrayList();
private final List propertyDescriptions = new ArrayList();
private final CharArrayWriter writer = new CharArrayWriter();
private boolean exhaustive = false;
/**
* If true, then stack trace is extracted for each exception. If false, the default, then stack
* trace is extracted for only the deepest exception.
*/
public boolean isExhaustive()
{
return exhaustive;
}
public void setExhaustive(boolean value)
{
exhaustive = value;
}
/**
* Analyzes the exceptions. This builds an {@link ExceptionDescription}for the exception. It
* also looks for a non-null {@link Throwable}property. If one exists, then a second
* {@link ExceptionDescription}is created. This continues until no more nested exceptions can
* be found.
* <p>
* The description includes a set of name/value properties (as {@link ExceptionProperty})
* object. This list contains all non-null properties that are not, themselves,
* {@link Throwable}.
* <p>
* The name is the display name (not the logical name) of the property. The value is the
* <code>toString()</code> value of the property. Only properties defined in subclasses of
* {@link Throwable}are included.
* <p>
* A future enhancement will be to alphabetically sort the properties by name.
*/
public ExceptionDescription[] analyze(Throwable exception)
{
Throwable thrown = exception;
try
{
while (thrown != null)
{
thrown = buildDescription(thrown);
}
ExceptionDescription[] result = new ExceptionDescription[exceptionDescriptions.size()];
return (ExceptionDescription[]) exceptionDescriptions.toArray(result);
}
finally
{
exceptionDescriptions.clear();
propertyDescriptions.clear();
writer.reset();
}
}
protected Throwable buildDescription(Throwable exception)
{
BeanInfo info;
Class exceptionClass;
ExceptionProperty property;
PropertyDescriptor[] descriptors;
PropertyDescriptor descriptor;
Throwable next = null;
int i;
Object value;
Method method;
ExceptionProperty[] properties;
ExceptionDescription description;
String stringValue;
String message;
String[] stackTrace = null;
propertyDescriptions.clear();
message = exception.getMessage();
exceptionClass = exception.getClass();
// Get properties, ignoring those in Throwable and higher
// (including the 'message' property).
try
{
info = Introspector.getBeanInfo(exceptionClass, Throwable.class);
}
catch (IntrospectionException e)
{
return null;
}
descriptors = info.getPropertyDescriptors();
for (i = 0; i < descriptors.length; i++)
{
descriptor = descriptors[i];
method = descriptor.getReadMethod();
if (method == null)
continue;
try
{
value = method.invoke(exception, null);
}
catch (Exception e)
{
continue;
}
if (value == null)
continue;
// Some annoying exceptions duplicate the message property
// (I'm talking to YOU SAXParseException), so just edit that out.
if (message != null && message.equals(value))
continue;
// Skip Throwables ... but the first non-null found is the next
// exception (unless it refers to the current one - some 3rd party
// libaries do this). We kind of count on there being no more
// than one Throwable property per Exception.
if (value instanceof Throwable)
{
if (next == null && value != exception)
next = (Throwable) value;
continue;
}
stringValue = value.toString().trim();
if (stringValue.length() == 0)
continue;
property = new ExceptionProperty(descriptor.getDisplayName(), value);
propertyDescriptions.add(property);
}
// If exhaustive, or in the deepest exception (where there's no next)
// the extract the stack trace.
if (next == null || exhaustive)
stackTrace = getStackTrace(exception);
// Would be nice to sort the properties here.
properties = new ExceptionProperty[propertyDescriptions.size()];
ExceptionProperty[] propArray = (ExceptionProperty[]) propertyDescriptions
.toArray(properties);
description = new ExceptionDescription(exceptionClass.getName(), message, propArray,
stackTrace);
exceptionDescriptions.add(description);
return next;
}
/**
* Gets the stack trace for the exception, and converts it into an array of strings.
* <p>
* This involves parsing the string generated indirectly from
* <code>Throwable.printStackTrace(PrintWriter)</code>. This method can get confused if the
* message (presumably, the first line emitted by printStackTrace()) spans multiple lines.
* <p>
* Different JVMs format the exception in different ways.
* <p>
* A possible expansion would be more flexibility in defining the pattern used. Hopefully all
* 'mainstream' JVMs are close enough for this to continue working.
*/
protected String[] getStackTrace(Throwable exception)
{
writer.reset();
PrintWriter printWriter = new PrintWriter(writer);
exception.printStackTrace(printWriter);
printWriter.close();
String fullTrace = writer.toString();
writer.reset();
// OK, the trick is to convert the full trace into an array of stack frames.
StringReader stringReader = new StringReader(fullTrace);
LineNumberReader lineReader = new LineNumberReader(stringReader);
int lineNumber = 0;
List frames = new ArrayList();
try
{
while (true)
{
String line = lineReader.readLine();
if (line == null)
break;
// Always ignore the first line.
if (++lineNumber == 1)
continue;
frames.add(stripFrame(line));
}
lineReader.close();
}
catch (IOException ex)
{
// Not likely to happen with this particular set
// of readers.
}
String[] result = new String[frames.size()];
return (String[]) frames.toArray(result);
}
/**
* Sun's JVM prefixes each line in the stack trace with " <tab>at</tab> ", other JVMs don't. This
* method looks for and strips such stuff.
*/
private String stripFrame(String frame)
{
char[] array = frame.toCharArray();
int i = 0;
int state = SKIP_LEADING_WHITESPACE;
boolean more = true;
while (more)
{
// Ran out of characters to skip? Return the empty string.
if (i == array.length)
return "";
char ch = array[i];
switch (state)
{
// Ignore whitespace at the start of the line.
case SKIP_LEADING_WHITESPACE:
if (Character.isWhitespace(ch))
{
i++;
continue;
}
if (ch == 'a')
{
state = SKIP_T;
i++;
continue;
}
// Found non-whitespace, not 'a'
more = false;
break;
// Skip over the 't' after an 'a'
case SKIP_T:
if (ch == 't')
{
state = SKIP_OTHER_WHITESPACE;
i++;
continue;
}
// Back out the skipped-over 'a'
i--;
more = false;
break;
// Skip whitespace between 'at' and the name of the class
case SKIP_OTHER_WHITESPACE:
if (Character.isWhitespace(ch))
{
i++;
continue;
}
// Not whitespace
more = false;
break;
}
}
// Found nothing to strip out.
if (i == 0)
return frame;
return frame.substring(i);
}
/**
* Produces a text based exception report to the provided stream.
*/
public void reportException(Throwable exception, PrintStream stream)
{
int i;
int j;
ExceptionDescription[] descriptions;
ExceptionProperty[] properties;
String[] stackTrace;
String message;
descriptions = analyze(exception);
for (i = 0; i < descriptions.length; i++)
{
message = descriptions[i].getMessage();
if (message == null)
stream.println(descriptions[i].getExceptionClassName());
else
stream.println(descriptions[i].getExceptionClassName() + ": "
+ descriptions[i].getMessage());
properties = descriptions[i].getProperties();
for (j = 0; j < properties.length; j++)
stream.println(" " + properties[j].getName() + ": " + properties[j].getValue());
// Just show the stack trace on the deepest exception.
if (i + 1 == descriptions.length)
{
stackTrace = descriptions[i].getStackTrace();
for (j = 0; j < stackTrace.length; j++)
stream.println(stackTrace[j]);
}
else
stream.println();
}
}
}