Skip to content

Instantly share code, notes, and snippets.

@hugithordarson
Last active January 11, 2017 14:36
Show Gist options
  • Save hugithordarson/4dcb8a9a16920333d3f250d0be6d605a to your computer and use it in GitHub Desktop.
Save hugithordarson/4dcb8a9a16920333d3f250d0be6d605a to your computer and use it in GitHub Desktop.
Improved exception page for WebObjects applications, sources and example use.
package is.rebbi.wo.components.error;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WOResponse;
import com.webobjects.foundation.NSBundle;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.development.NSMavenProjectBundle;
import com.webobjects.woextensions.WOExceptionParser;
import com.webobjects.woextensions.WOParsedErrorLine;
import er.extensions.appserver.ERXApplication;
import er.extensions.components.ERXComponent;
/**
* A little nicer version of WOExceptionPage.
*
* TODO: Allow clicking to display source in browser for files where it's available.
* TODO: Allow clicking in browser to open source file and go to line in Eclipse.
* TODO: Allow reading of sources files that are not in bundles, but in JAR files.
* TODO: Decompile sources if no source file available.
*/
public class USExceptionPage extends ERXComponent {
private static final Logger logger = LoggerFactory.getLogger( USExceptionPage.class );
private static final int NUMBER_OF_LINES_BEFORE_ERROR_LINE = 7;
private static final int NUMBER_OF_LINES_AFTER_ERROR_LINE = 7;
/**
* The exception we're reporting.
*/
private Throwable _exception;
/**
* Line of source file currently being iterated over in the view.
*/
public String currentSourceLine;
/**
* Current index of the source line iteration.
*/
public int currentSourceLineIndex;
/**
* WO class that parses the stack trace for us.
*/
public WOExceptionParser exceptionParser;
/**
* Line of the stack trace currently being iterated over.
*/
public WOParsedErrorLine currentErrorLine;
public USExceptionPage( WOContext aContext ) {
super( aContext );
}
/**
* @return First line of the stack trace, essentially the causing line.
*/
public WOParsedErrorLine firstLineOfTrace() {
List<WOParsedErrorLine> stackTrace = exceptionParser.stackTrace();
if( stackTrace.isEmpty() ) {
return null;
}
return stackTrace.get( 0 );
}
/**
* @return true if source should be shown.
*/
public boolean showSource() {
return ERXApplication.isDevelopmentModeSafe() && sourceFileContainingError() != null;
}
/**
* @return The source file where the exception originated (from the last line of the stack trace).
*/
private Path sourceFileContainingError() {
String nameOfThrowingClass = firstLineOfTrace().packageClassPath();
NSBundle bundle = bundleForClassName( nameOfThrowingClass );
if( bundle == null ) {
return null;
}
String path = null;
if( NSBundle.mainBundle() instanceof NSMavenProjectBundle ) {
path = bundle.bundlePath() + "/src/main/java/" + nameOfThrowingClass.replace( ".", "/" ) + ".java";
}
else {
path = bundle.bundlePath() + "/Sources/" + nameOfThrowingClass.replace( ".", "/" ) + ".java";
}
return Paths.get( path );
}
/**
* @return The source lines to view in the browser.
*/
public List<String> lines() {
List<String> lines;
try {
lines = Files.readAllLines( sourceFileContainingError() );
}
catch( IOException e ) {
logger.error( "Attempt to read source code from '{}' failed", sourceFileContainingError(), e );
return new ArrayList<>();
}
int indexOfFirstLineToShow = firstLineOfTrace().line() - NUMBER_OF_LINES_BEFORE_ERROR_LINE;
int indexOfLastLineToShow = firstLineOfTrace().line() + NUMBER_OF_LINES_AFTER_ERROR_LINE;
if( indexOfFirstLineToShow < 0 ) {
indexOfFirstLineToShow = 0;
}
if( indexOfLastLineToShow > lines.size() ) {
indexOfLastLineToShow = lines.size();
}
return lines.subList( indexOfFirstLineToShow, indexOfLastLineToShow );
}
/**
* @return Actual number of source file line being iterated over in the view.
*/
public int currentActualLineNumber() {
return firstLineOfTrace().line() - NUMBER_OF_LINES_BEFORE_ERROR_LINE + currentSourceLineIndex + 1;
}
/**
* @return CSS class for the current line of the source file (to show odd/even lines and highlight the error line)
*/
public String sourceLineClass() {
List<String> cssClasses = new ArrayList<>();
cssClasses.add( "src-line" );
if( currentSourceLineIndex % 2 == 0 ) {
cssClasses.add( "even-line" );
}
else {
cssClasses.add( "odd-line" );
}
if( isLineContainingError() ) {
cssClasses.add( "error-line" );
}
return String.join( " ", cssClasses );
}
/**
* @return true if the current line being iterated over is the line containining the error.
*/
private boolean isLineContainingError() {
return currentSourceLineIndex == NUMBER_OF_LINES_BEFORE_ERROR_LINE - 1;
}
public Throwable exception() {
return _exception;
}
public void setException( Throwable value ) {
exceptionParser = new WOExceptionParser( value );
_exception = value;
}
/**
* @return bundle of the class currently being iterated over in the UI (if any)
*/
public NSBundle currentBundle() {
return bundleForClassName( currentErrorLine.packageClassPath() );
}
/**
* Provided for convenience, to use this exception page when overriding Application.reportException(). Like so:
*
* @Override
* public WOResponse reportException( Throwable exception, WOContext context, NSDictionary extraInfo ) {
* return USExceptionPage.reportException( exception, context, extraInfo );
* }
*/
public static WOResponse reportException( Throwable exception, WOContext context, NSDictionary extraInfo ) {
USExceptionPage nextPage = ERXApplication.erxApplication().pageWithName( USExceptionPage.class, context );
nextPage.setException( exception );
return nextPage.generateResponse();
}
/**
* @return The bundle containing the (fully qualified) named class. Null if class is not found or not contained in a bundle.
*/
private static NSBundle bundleForClassName( String fullyQualifiedClassName ) {
Class<?> clazz;
try {
clazz = Class.forName( fullyQualifiedClassName );
}
catch( ClassNotFoundException e ) {
return null;
}
return NSBundle.bundleForClass( clazz );
}
/**
* @return The CSS class of the current row in the stack trace table.
*/
public String currentRowClass() {
if( NSBundle.mainBundle().equals( currentBundle() ) ) {
return "success";
}
return null;
}
}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>An exception occurred</title>
<style type="text/css">
body, table, td, th {
font-family: sans-serif;
font-size: 12px;
}
table {
width: 100%;
border: 1px solid rgb(200,200,200);
border-collapse: collapse;
}
td,th {
margin: 0px;
padding: 5px 10px;
text-align: left;
white-space: nowrap;
}
th {
background-color: rgb(230,230,230);
}
table tr:nth-child(odd) {
background-color: rgb(250,250,250);
}
table tr:nth-child(even) {
background-color: rgb(240,240,240);
}
.filename-container {
font-family: monospace;
color: gray;
}
tr.success {
background-color: rgb(200,240,200) !important;
}
.container {
width: 90%;
margin: auto;
}
.src-container {
font-family: monospace;
font-size: small;
color: rgb(50,50,50);
border: 1px solid rgb(200,200,200);
padding: 0px;
margin: 10px 0px;
overflow: hidden;
}
.src-line {
white-space: pre;
display: block;
}
.src-line>span {
display: inline-block;
background-color: rgba(0,0,0,0.1);
padding: 0px 2px;
}
.odd-line {
background-color: rgb( 250,250,250);
}
.even-line {
background-color: rgb( 240,240,240);
}
.error-line {
background-color: rgb(255,180,180);
}
</style>
</head>
<body>
<div class="container">
<h3><wo:str value="$exception.class.name" /><wo:if condition="$exception.cause"><span style="font-size: smaller; color: gray">(<wo:str value="$exception.cause" />)</span></wo:if></h3>
<p><wo:str value="$exceptionParser.message" /></p>
<wo:if condition="$showSource">
<div class="filename-container"><strong><wo:str value="$firstLineOfTrace.fileName" /></strong></div>
<div class="src-container">
<wo:repetition list="$lines" item="$currentSourceLine" index="$currentSourceLineIndex"><wo:container elementName="span" class="$sourceLineClass"><span><wo:str value="$currentActualLineNumber" /></span> <wo:str value="$currentSourceLine" /></wo:container></wo:repetition>
</div>
</wo:if>
<table class="table table-condensed table-bordered table-striped">
<tr>
<th>File</th>
<th>Line #</th>
<th>Method</th>
<th>Package</th>
<th>Bundle</th>
</tr>
<wo:repetition list="$exceptionParser.stackTrace" item="$currentErrorLine">
<wo:container elementName="tr" class="$currentRowClass">
<td><wo:str value="$currentErrorLine.fileName" /></td>
<td><wo:str value="$currentErrorLine.lineNumber" /></td>
<td><wo:str value="$currentErrorLine.methodName" /></td>
<td><wo:str value="$currentErrorLine.packageName" /></td>
<td><wo:str value="$currentBundle.name" /></td>
</wo:container>
</wo:repetition>
</table>
<div align="right"><h4 style="font-weight: normal; color: gray">The exception occurred in the application <strong><wo:str value="$application.name" /></strong></h4></div>
</div>
</body>
</html>
package custodian;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WOResponse;
import com.webobjects.foundation.NSDictionary;
import er.extensions.appserver.ERXApplication;
import is.rebbi.wo.components.error.USExceptionPage;
public class Application extends ERXApplication {
public static void main( String[] argv ) {
ERXApplication.main( argv, Application.class );
}
@Override
public WOResponse reportException( Throwable exception, WOContext context, NSDictionary extraInfo ) {
return USExceptionPage.reportException( exception, context, extraInfo );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment