package er.extensions.components; 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 nicer version of WOExceptionPage. * * When in development mode, it will show java code where exception occurred (highlighting the exact line) */ public class ERXExceptionPage extends ERXComponent { private static final Logger logger = LoggerFactory.getLogger( ERXExceptionPage.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; /** * A path modifier to put between bundle path and modules so that source code locations outside the * regular path can be accommodated. */ private String pathModifier; public ERXExceptionPage( WOContext aContext ) { super( aContext ); pathModifier = ""; } /** * Specifying a path fragment here will insert that between bundle path and source module path. Example * modifier: "/../.." to go two directories up. May be needed for non-standard workspace setups. * * @param modifier modifier to insert, should start with a "/" * */ public void setPathModifier( String modifier ) { pathModifier = modifier; } /** * @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() + pathModifier + "/src/main/java/" + nameOfThrowingClass.replace( ".", "/" ) + ".java"; } else { path = bundle.bundlePath() + pathModifier + "/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 when overriding Application.reportException(). Like so: * * @Override * public WOResponse reportException( Throwable exception, WOContext context, NSDictionary extraInfo ) { * return ERXExceptionPage.reportException( exception, context, extraInfo ); * } */ public static WOResponse reportException( Throwable exception, WOContext context, NSDictionary extraInfo ) { ERXExceptionPage nextPage = ERXApplication.erxApplication().pageWithName( ERXExceptionPage.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; } }