| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.click.util; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.LineNumberReader; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.Enumeration; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.ResourceBundle; |
| import java.util.StringTokenizer; |
| import java.util.TreeMap; |
| |
| import javax.servlet.ServletContext; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpSession; |
| |
| import org.apache.click.Page; |
| import org.apache.click.service.TemplateException; |
| import org.apache.commons.lang.StringUtils; |
| |
| /** |
| * Provides an HTML <div> error report for the display of page error |
| * information. This class is used by ErrorPage and ClickServlet for the |
| * display of error information. |
| */ |
| public class ErrorReport { |
| |
| /** The Java language keywords. Used to render Java source code. */ |
| private static final String[] JAVA_KEYWORDS = { "package", "import", |
| "class", "public", "protected", "private", "extends", "implements", |
| "return", "if", "while", "for", "do", "else", "try", "new", "void", |
| "catch", "throws", "throw", "static", "final", "break", "continue", |
| "super", "finally", "true", "false", "true;", "false;", "null", |
| "boolean", "int", "char", "long", "float", "double", "short" }; |
| |
| /** The column number of the error, or -1 if not defined. */ |
| protected int columnNumber; |
| |
| /** The cause of the error. */ |
| protected final Throwable error; |
| |
| |
| /** The line number of the error, or -1 if not defined. */ |
| protected int lineNumber; |
| |
| /** The error is Velocity parsing exception. */ |
| protected final boolean isParseError; |
| |
| /** The application is in "production" mode flag. */ |
| protected final boolean isProductionMode; |
| |
| /** The page class which caused the error. */ |
| protected final Class<? extends Page> pageClass; |
| |
| /** The servlet request. */ |
| protected final HttpServletRequest request; |
| |
| /** The servlet context. */ |
| protected final ServletContext servletContext; |
| |
| /** The name of the error source. */ |
| protected final String sourceName; |
| |
| /** The error source LineNumberReader. */ |
| protected LineNumberReader sourceReader; |
| |
| // Constructor ------------------------------------------------------------ |
| |
| /** |
| * Create a ErrorReport instance from the given error and page. |
| * |
| * @param error the cause of the error |
| * @param pageClass the Page class which caused the error |
| * @param isProductionMode the application is in "production" mode |
| * @param request the page request |
| * @param servletContext the servlet context |
| */ |
| public ErrorReport(Throwable error, Class<? extends Page> pageClass, |
| boolean isProductionMode, HttpServletRequest request, |
| ServletContext servletContext) { |
| |
| this.error = error; |
| this.pageClass = pageClass; |
| this.isProductionMode = isProductionMode; |
| this.request = request; |
| this.servletContext = servletContext; |
| |
| if (error instanceof TemplateException |
| && ((TemplateException) error).isParseError()) { |
| |
| TemplateException te = (TemplateException) error; |
| |
| if (te.getTemplateName().charAt(0) == '/') { |
| sourceName = te.getTemplateName(); |
| } else { |
| sourceName = '/' + te.getTemplateName(); |
| } |
| lineNumber = te.getLineNumber(); |
| columnNumber = te.getColumnNumber(); |
| |
| InputStream is = |
| servletContext.getResourceAsStream(sourceName); |
| |
| sourceReader = new LineNumberReader(new InputStreamReader(is)); |
| isParseError = true; |
| |
| } else { |
| isParseError = false; |
| sourceName = null; |
| columnNumber = -1; |
| |
| StringWriter sw = new StringWriter(); |
| PrintWriter pw = new PrintWriter(sw); |
| getCause().printStackTrace(pw); |
| |
| StringTokenizer tokenizer = |
| new StringTokenizer(sw.toString(), "\n"); |
| |
| try { |
| tokenizer.nextToken(); |
| String line = tokenizer.nextToken(); |
| if (getCause() instanceof IllegalArgumentException) { |
| line = tokenizer.nextToken(); |
| |
| if (line.contains("org.apache.commons.lang.Validate")) { |
| line = tokenizer.nextToken(); |
| } |
| } |
| |
| int nameStart = line.indexOf("at "); |
| int nameEnd = line.indexOf("("); |
| nameEnd = line.lastIndexOf(".", nameEnd); |
| if (line.contains("$")) { |
| nameEnd = line.indexOf("$"); |
| } |
| |
| if (nameStart != -1 && nameEnd != -1) { |
| String classname = line.substring(nameStart + 3, nameEnd); |
| |
| int lineStart = line.indexOf(":"); |
| if (lineStart != -1) { |
| int lineEnd = line.indexOf(")"); |
| String lineNumber = line.substring(lineStart + 1, lineEnd); |
| |
| this.lineNumber = Integer.parseInt(lineNumber); |
| |
| String filename = |
| "/" + classname.replace('.', '/') + ".java"; |
| |
| sourceReader = getJavaSourceReader(filename); |
| } |
| } |
| |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| // Public Methods --------------------------------------------------------- |
| |
| /** |
| * Return a error report HTML <div> element for the given error and |
| * page. The HTML <div> element 'id' and 'class' attribute values are |
| * 'errorReport'. |
| * |
| * @return a error HTML display string |
| */ |
| @SuppressWarnings("unchecked") |
| public String toString() { |
| |
| if (isProductionMode()) { |
| Locale locale = request.getLocale(); |
| ResourceBundle bundle = ClickUtils.getBundle("click-control", locale); |
| return bundle.getString("production-error-message"); |
| } |
| |
| HtmlStringBuffer buffer = new HtmlStringBuffer(10 * 1024); |
| |
| Throwable cause = getCause(); |
| |
| buffer.append("<div id='errorReport' class='errorReport'>\n"); |
| |
| // Exception table |
| buffer.append("<table border='1' cellspacing='1' cellpadding='4' width='100%' style='background-color: white;'>"); |
| if (isParseError()) { |
| buffer.append("<tr><td colspan='2' style='color:white; background-color: navy; font-weight: bold'>Page Parsing Error</td></tr>"); |
| buffer.append("<tr><td width='12%'><b>Source</b></td><td>"); |
| buffer.append(getSourceName()); |
| buffer.append("</td></tr>"); |
| } else { |
| buffer.append("<tr><td colspan='2' style='color:white; background-color: navy; font-weight: bold'>Exception</td></tr>"); |
| buffer.append("<tr><td width='12%'><b>Class</b></td><td>"); |
| buffer.append(cause.getClass().getName()); |
| buffer.append("</td></tr>"); |
| } |
| buffer.append("<tr><td valign='top' width='12%'><b>Message</b></td><td>"); |
| buffer.append(getMessage()); |
| buffer.append("</td></tr>"); |
| if (getSourceReader() != null) { |
| buffer.append("<tr><td valign='top' colspan='2'>\n"); |
| buffer.append(getRenderedSource()); |
| buffer.append("</td></tr>"); |
| } |
| if (!isParseError()) { |
| buffer.append("<tr><td valign='top' colspan='2'>\n"); |
| buffer.append(getStackTrace()); |
| buffer.append("</td></tr>"); |
| } |
| buffer.append("</table>"); |
| buffer.append("<br/>"); |
| |
| // Page table |
| buffer.append("<table border='1' cellspacing='1' cellpadding='4' width='100%' style='background-color: white;'>"); |
| buffer.append("<tr><td colspan='2' style='color:white; background-color: navy; font-weight: bold'>Page</td></tr>"); |
| buffer.append("<tr><td width='12%'><b>Classname</b></td><td>"); |
| if (pageClass != null) { |
| buffer.append(pageClass.getName()); |
| } |
| buffer.append("</td></tr>"); |
| buffer.append("<tr><td width='12%'><b>Path</b></td><td>"); |
| buffer.append(ClickUtils.getResourcePath(request)); |
| buffer.append("</td></tr>"); |
| buffer.append("</table>"); |
| buffer.append("<br/>"); |
| |
| // Request table |
| buffer.append("<table border='1' cellspacing='1' cellpadding='4' width='100%' style='background-color: white;'>"); |
| buffer.append("<tr><td colspan='2' style='color:white; background-color: navy; font-weight: bold'>Request</td></tr>"); |
| |
| Map<String, Object> requestAttributes = new TreeMap<String, Object>(); |
| Enumeration attributeNames = request.getAttributeNames(); |
| while (attributeNames.hasMoreElements()) { |
| String name = attributeNames.nextElement().toString(); |
| requestAttributes.put(name, request.getAttribute(name)); |
| } |
| buffer.append("<tr><td width='12%' valign='top'><b>Attributes</b></td><td>"); |
| writeMap(requestAttributes, buffer); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td width='12%'><b>Auth Type</b></td><td>"); |
| buffer.append(request.getAuthType()); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td width='12%'><b>Context Path</b></td><td>"); |
| buffer.append("<a href='"); |
| buffer.append(request.getContextPath()); |
| buffer.append("'>"); |
| buffer.append(request.getContextPath()); |
| buffer.append("</a>"); |
| buffer.append("</td></tr>"); |
| |
| Map<String, Object> requestHeaders = new TreeMap<String, Object>(); |
| Enumeration headerNames = request.getHeaderNames(); |
| while (headerNames.hasMoreElements()) { |
| String name = headerNames.nextElement().toString(); |
| requestHeaders.put(name, request.getHeader(name)); |
| } |
| buffer.append("<tr><td width='12%' valign='top'><b>Headers</b></td><td>"); |
| writeMap(requestHeaders, buffer); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td width='12%'><b>Query</b></td><td>"); |
| String escapedQueryString = ClickUtils.escape(request.getQueryString()); |
| buffer.append(escapedQueryString); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td width='12%'><b>Method</b></td><td>"); |
| buffer.append(request.getMethod()); |
| buffer.append("</td></tr>"); |
| |
| Map<String, Object> requestParams = new TreeMap<String, Object>(); |
| Enumeration paramNames = request.getParameterNames(); |
| while (paramNames.hasMoreElements()) { |
| String name = paramNames.nextElement().toString(); |
| requestParams.put(name, request.getParameter(name)); |
| } |
| buffer.append("<tr><td width='12%' valign='top'><b>Parameters</b></td><td>"); |
| writeMap(requestParams, buffer); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td width='12%'><b>Remote User</b></td><td>"); |
| buffer.append(request.getRemoteUser()); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td width='12%' valign='top'><b>URI</b></td><td>"); |
| buffer.append("<a href='"); |
| String requestURI = ClickUtils.getRequestURI(request); |
| buffer.append(requestURI); |
| buffer.append("'>"); |
| buffer.append(requestURI); |
| buffer.append("</a>"); |
| buffer.append("</td></tr>"); |
| |
| buffer.append("<tr><td><b width='12%'>URL</b></td><td>"); |
| buffer.append("<a href='"); |
| buffer.append(request.getRequestURL()); |
| buffer.append("'>"); |
| buffer.append(request.getRequestURL()); |
| buffer.append("</a>"); |
| buffer.append("</td></tr>"); |
| |
| Map<String, Object> sessionAttributes = new TreeMap<String, Object>(); |
| if (request.getSession(false) != null) { |
| HttpSession session = request.getSession(); |
| attributeNames = session.getAttributeNames(); |
| while (attributeNames.hasMoreElements()) { |
| String name = attributeNames.nextElement().toString(); |
| sessionAttributes.put(name, session.getAttribute(name)); |
| } |
| } |
| buffer.append("<tr><td width='12%' valign='top'><b>Session</b></td><td>"); |
| writeMap(sessionAttributes, buffer); |
| buffer.append("</td></tr>"); |
| buffer.append("</table>\n"); |
| |
| buffer.append("</div>\n"); |
| |
| return buffer.toString(); |
| } |
| |
| // Protected Methods ------------------------------------------------------ |
| |
| /** |
| * Return the cause of the error. |
| * |
| * @return the cause of the error |
| */ |
| protected Throwable getCause() { |
| Throwable cause = null; |
| if (error instanceof ServletException) { |
| cause = ((ServletException) error).getRootCause(); |
| if (cause == null) { |
| cause = error.getCause(); |
| } |
| } else { |
| cause = error.getCause(); |
| } |
| if (cause != null && cause.getCause() != null) { |
| cause = cause.getCause(); |
| } |
| if (cause == null) { |
| cause = error; |
| } |
| return cause; |
| } |
| |
| /** |
| * Return the error source column number, or -1 if not determined. |
| * |
| * @return the error source column number, or -1 if not determined |
| */ |
| protected int getColumnNumber() { |
| return columnNumber; |
| } |
| |
| /** |
| * Return true if the error was a Velocity parsing exception. |
| * |
| * @return true if the error was a Velocity parsing exception |
| */ |
| protected boolean isParseError() { |
| return isParseError; |
| } |
| |
| /** |
| * Return true if the application is in "production" mode. |
| * |
| * @return true if the application is in "production" mode |
| */ |
| protected boolean isProductionMode() { |
| return isProductionMode; |
| } |
| |
| /** |
| * Return the error source line number, or -1 if not determined. |
| * |
| * @return the error source line number, or -1 if not determined |
| */ |
| protected int getLineNumber() { |
| return lineNumber; |
| } |
| |
| /** |
| * Return the error message. |
| * |
| * @return the error message |
| */ |
| protected String getMessage() { |
| if (isParseError()) { |
| String message = error.getMessage(); |
| |
| String parseMsg = message; |
| int startIndex = message.indexOf('\n'); |
| int endIndex = message.lastIndexOf("..."); |
| |
| if (startIndex != -1 && endIndex > startIndex) { |
| parseMsg = message.substring(startIndex + 1, endIndex); |
| } |
| |
| parseMsg = ClickUtils.escapeHtml(parseMsg); |
| |
| parseMsg = StringUtils.replace(parseMsg, "...", ",  "); |
| |
| return parseMsg; |
| |
| } else { |
| Throwable cause = getCause(); |
| |
| String value = |
| (cause.getMessage() != null) ? cause.getMessage() : "null"; |
| |
| return ClickUtils.escapeHtml(value); |
| } |
| } |
| |
| /** |
| * Return the error source name. |
| * |
| * @return the error source name |
| */ |
| protected String getSourceName() { |
| return sourceName; |
| } |
| |
| /** |
| * Return a LineNumberReader for the error source file, or null if not |
| * defined. |
| * |
| * @return LineNumberReader for the error source file, or null if not |
| * defined |
| */ |
| protected LineNumberReader getSourceReader() { |
| return sourceReader; |
| } |
| |
| /** |
| * Return Java Source LineNumberReader for the given filename, or null if |
| * not found. |
| * |
| * @param filename the name of the Java source file, e.g. /examples/Page.java |
| * @return LineNumberReader for the given source filename, or null if |
| * not found |
| * @throws FileNotFoundException if file could not be found |
| */ |
| protected LineNumberReader getJavaSourceReader(String filename) |
| throws FileNotFoundException { |
| |
| if (pageClass == null) { |
| return null; |
| } |
| |
| // Look for source file on classpath |
| InputStream is = pageClass.getResourceAsStream(filename); |
| if (is != null) { |
| return new LineNumberReader(new InputStreamReader(is)); |
| } |
| |
| // Else search for source file under WEB-INF |
| String rootPath = servletContext.getRealPath("/"); |
| |
| String webInfPath = rootPath + File.separator + "WEB-INF"; |
| |
| File sourceFile = null; |
| |
| File webInfDir = new File(webInfPath); |
| if (webInfDir.isDirectory() && webInfDir.canRead()) { |
| File[] dirList = webInfDir.listFiles(); |
| if (dirList != null) { |
| for (File file : dirList) { |
| if (file.isDirectory() && file.canRead()) { |
| String sourcePath = file.toString() + filename; |
| sourceFile = new File(sourcePath); |
| if (sourceFile.isFile() && sourceFile.canRead()) { |
| |
| FileInputStream fis = new FileInputStream(sourceFile); |
| return new LineNumberReader(new InputStreamReader(fis)); |
| } |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Return a HTML rendered section of the source error with the error |
| * line highlighted. |
| * |
| * @return a HTML rendered section of the parsing error page template |
| */ |
| protected String getRenderedSource() { |
| |
| HtmlStringBuffer buffer = new HtmlStringBuffer(5 * 1024); |
| |
| buffer.append("<pre style='font-family: Courier New, courier;'>"); |
| |
| if (sourceReader == null) { |
| buffer.append("Source is not available.</span>"); |
| return buffer.toString(); |
| } |
| |
| final String errorLineStyle = |
| "style='background-color:yellow;'"; |
| |
| final String errorCharSpan = |
| "<span style='color:red;text-decoration:underline;font-weight:bold;'>"; |
| |
| try { |
| String line = sourceReader.readLine(); |
| |
| while (line != null) { |
| if (skipLine()) { |
| line = sourceReader.readLine(); |
| continue; |
| } |
| |
| boolean isErrorLine = |
| sourceReader.getLineNumber() == lineNumber; |
| |
| // Start div tag |
| buffer.append("<div "); |
| if (isErrorLine) { |
| buffer.append(errorLineStyle); |
| } |
| buffer.append(">"); |
| |
| // Write out line number |
| String lineStr = "" + sourceReader.getLineNumber(); |
| int numberSpace = 3 - lineStr.length(); |
| for (int i = 0; i < numberSpace; i++) { |
| buffer.append(" "); |
| } |
| if (isErrorLine) { |
| buffer.append("<b>"); |
| } |
| buffer.append(sourceReader.getLineNumber()); |
| if (isErrorLine) { |
| buffer.append("</b>"); |
| } |
| buffer.append(": "); |
| |
| // Write out line content |
| if (isErrorLine) { |
| if (isParseError()) { |
| HtmlStringBuffer htmlLine = |
| new HtmlStringBuffer(line.length() * 2); |
| |
| for (int i = 0; i < line.length(); i++) { |
| if (i == getColumnNumber() - 1) { |
| htmlLine.append(errorCharSpan); |
| htmlLine.append(line.charAt(i)); |
| htmlLine.append("</span>"); |
| } else { |
| htmlLine.append(line.charAt(i)); |
| } |
| } |
| buffer.append(htmlLine.toString()); |
| |
| } else { |
| buffer.append(getRenderJavaLine(line)); |
| } |
| |
| } else { |
| if (isParseError()) { |
| buffer.append(ClickUtils.escapeHtml(line)); |
| } else { |
| buffer.append(getRenderJavaLine(line)); |
| } |
| } |
| |
| // Close div tag |
| buffer.append("</div>"); |
| |
| line = sourceReader.readLine(); |
| } |
| |
| } catch (IOException ioe) { |
| buffer.append("Could not load page source: "); |
| buffer.append(ClickUtils.escapeHtml(ioe.toString())); |
| } finally { |
| try { |
| sourceReader.close(); |
| } catch (IOException ioe) { |
| // do nothing |
| } |
| } |
| |
| buffer.append("</pre>"); |
| |
| return buffer.toString(); |
| } |
| |
| /** |
| * Return a HTML encode stack trace string from the given error. |
| * |
| * @return a HTML encode stack trace string. |
| */ |
| protected String getStackTrace() { |
| |
| StringWriter sw = new StringWriter(); |
| PrintWriter pw = new PrintWriter(sw); |
| getCause().printStackTrace(pw); |
| |
| HtmlStringBuffer buffer |
| = new HtmlStringBuffer(sw.toString().length() + 80); |
| buffer.append("<pre><tt style='font-size:10pt;'>"); |
| buffer.append(ClickUtils.escapeHtml(sw.toString().trim())); |
| buffer.append("</tt></pre>"); |
| |
| return buffer.toString(); |
| } |
| |
| /** |
| * Write out the map name value pairs as name=value lines to the string |
| * buffer. |
| * |
| * @param map the Map of name value pairs |
| * @param buffer the string buffer to write out the values to |
| */ |
| protected void writeMap(Map<String, Object> map, HtmlStringBuffer buffer) { |
| for (Map.Entry<String, Object> entry : map.entrySet()) { |
| String key = entry.getKey(); |
| Object value = entry.getValue(); |
| |
| buffer.append(key); |
| buffer.append("="); |
| if (value != null) { |
| buffer.append(ClickUtils.escapeHtml(value.toString())); |
| } else { |
| buffer.append("null"); |
| } |
| buffer.append("<br/>"); |
| } |
| if (map.isEmpty()) { |
| buffer.append(" "); |
| } |
| } |
| |
| /** |
| * Return a HTML rendered Java source line with keywords highlighted |
| * using the given line. |
| * |
| * @param line the Java source line to render |
| * @return HTML rendered Java source line |
| */ |
| protected String getRenderJavaLine(String line) { |
| line = ClickUtils.escapeHtml(line); |
| |
| for (String keyword : JAVA_KEYWORDS) { |
| line = renderJavaKeywords(line, keyword); |
| } |
| |
| return line; |
| } |
| |
| /** |
| * Render the HTML rendered Java source line with the given keyword |
| * highlighted. |
| * |
| * @param line the given Java source line to render |
| * @param keyword the Java keyword to highlight |
| * @return the HTML rendered Java source line with the given keyword |
| * highlighted |
| */ |
| protected String renderJavaKeywords(String line, String keyword) { |
| String markupToken = |
| "<span style='color:#7f0055;font-weight:bold;'>" + keyword + "</span>"; |
| |
| line = StringUtils.replace(line, |
| " " + keyword + " ", |
| " " + markupToken + " "); |
| |
| if (line.startsWith(keyword)) { |
| line = markupToken + line.substring(keyword.length()); |
| } |
| |
| if (line.endsWith(keyword)) { |
| line = line.substring(0, line.length() - keyword.length()) |
| + markupToken; |
| } |
| |
| return line; |
| } |
| |
| /** |
| * Return true if the current line read from the source line reader, should |
| * be skipped, or false if the current line should be rendered. |
| * |
| * @return true if the current line from the source reader should be skipped |
| */ |
| protected boolean skipLine() { |
| final int currentLine = getSourceReader().getLineNumber(); |
| final int errorLine = getLineNumber(); |
| |
| if (Math.abs(currentLine - errorLine) <= 10) { |
| return false; |
| } else { |
| return true; |
| } |
| } |
| } |