Hi Howard,
I read your blog and I'd like to make a request. I'd really like to read
more technical details on techniques you might use to get line precise
error reporting.
This is one of the best features of tapestry and if you can pass on some of
the details of how to go about it and get people excited about doing it
themselves that could only be a good thing. This is typically an area
where most other open source projects fail badly. The normal error
reporting seems to be the null pointer exception.
Regards,
Glen Stampoultzis
gstamp@iinet.net.au
http://www.jroller.com/page/gstamp
I agree with all of this, but it's not just open source projects which fall flat in this area ... and this is a vitally important area: Feedback, one of my four key aspects of a useful framework (along with Simplicity, Efficiency and Consistency). Without good feedback, the developer will be faced with a challenging, time-consuming puzzle every time something goes wrong in the framework code. It's not enough to say "garbage in, garbage out" ... if the framework makes getting the job done harder, or even just makes it seem harder, then it won't get used, regardless of what other benefits it provides.
Line precise error reporting is not magic, but it is a cross-cutting concern (Dion is thinking about how to make it an aspect), so it touches a lot of code.
It starts with the Resource interface, which is an abstraction around files; files stored on the file system, at a URL, or within the classpath. Tapestry extends Resource further, adding the concept of a file within a web application context. The Location interface builds on this, combining a resource with lineNumber and columnNumber properties.
The XML, HTML and SDL parsers used by both HiveMind and Tapestry carefully track the location (in most cases, by making use of the SAX Locator to figure out where in a file the parser currently is).
All the various descriptor (in HiveMind) and specification (in Tapestry) objects implement the
Locatable interface (having a readable location property), or even the LocationHolder interface (having a writable location property), typically by
extending from BaseLocatable. As the parsers create these objects, they are tagged with the current location provided by the parser.
Later, runtime objects (services and such in HiveMind, components and such in Tapestry) are created from the descriptor/specification
You can see how my naming has been evolving. Descriptor is a better term, I don't remember where "specification" came from, but
it's now entrenched in Tapestry terminology.
objects. The runtime objects also implement LocationHolder, and the location of the descriptor object is copied into the location property of the runtime object.
The next piece of the puzzle is that exceptions need to have a location as well! When an exception occurs in a runtime object, the runtime object throws an ApplicationRuntimeException that includes the correct location.
There are a couple of utility methods on the HiveMind class used to help determine what the correct location is:
/**
* Selects the first {@link Location} in an array of objects.
* Skips over nulls. The objects may be instances of
* Location or {@link Locatable}. May return null
* if no Location can be found.
*/
public static Location findLocation(Object[] locations)
{
for (int i = 0; i < locations.length; i++)
{
Object location = locations[i];
Location result = getLocation(location);
if (result != null)
return result;
}
return null;
}
/**
* Extracts a location from an object, checking to see if it
* implement {@link Location} or {@link Locatable}.
*
* @returns the Location, or null if it can't be found
*/
public static Location getLocation(Object object)
{
if (object == null)
return null;
if (object instanceof Location)
return (Location) object;
if (object instanceof Locatable)
{
Locatable locatable = (Locatable) object;
return locatable.getLocation();
}
return null;
}
This method is particularly handy to the ApplicationRuntimeException class, since it may want to draw the location from an explicit constructor parameter, from a nested exception, or from an arbitrary "component" associated with the exception:
public ApplicationRuntimeException(
String message,
Object component,
Location location,
Throwable rootCause)
{
super(message);
_rootCause = rootCause;
_component = component;
_location = HiveMind.findLocation(new Object[] { location, rootCause, component });
}
That's pretty much all there is to it ... but it's all for naught if all this location information is not presented to the user. The location will generally "bubble up" to the top level exception, but you still want to be able to see that information. Tapestry's exception report page does a great job of this, as it displays the properties of each exception (including the location property), and then tunnels down the stack of nested exceptions (this is actually encapsulated inside the ExceptionAnalyzer class).
Line precise error reporting isn't the end all of Feedback. Malcolm Edgar, a one-time Tapestry committer, has been working on his own web framework (I believe for internal use at his company) ... it goes one step further, actually displaying the content of his equivalent to an HTML template and highlighting the line that's in error. That's raising the bar, but perhaps Tapestry will catch up to that some day.
Further, simply reporting locations isn't enough. If I pass a null value into a method that doesn't allow null, I want to see a detailed exception (You must supply a non-null value for parameter 'action'.) rather than a NullPointerException. A detailed exception gives me, the developer, a head start on actually fixing the problem. Explicit checking along these lines means that the location that's actually reported will be more accurate as well, especially considering that there's no way to attach a location to a NullPointerException.