Last active
January 11, 2017 14:36
-
-
Save hugithordarson/4dcb8a9a16920333d3f250d0be6d605a to your computer and use it in GitHub Desktop.
Improved exception page for WebObjects applications, sources and example use.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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