ErrorDispatcher.java

/*
 * 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.jasper.compiler;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;
import org.xml.sax.SAXException;

/**
 * Class responsible for dispatching JSP parse and javac compilation errors to the configured error handler.
 * <p>
 * This class is also responsible for localizing any error codes before they are passed on to the configured error
 * handler.
 * <p>
 * In the case of a Java compilation error, the compiler error message is parsed into an array of JavacErrorDetail
 * instances, which is passed on to the configured error handler.
 *
 * @author Jan Luehe
 * @author Kin-man Chung
 */
public class ErrorDispatcher {

    /**
     * Custom error handler
     */
    private final ErrorHandler errHandler;

    /**
     * Indicates whether the compilation was initiated by JspServlet or JspC
     */
    private final boolean jspcMode;


    /**
     * Constructor.
     *
     * @param jspcMode true if compilation has been initiated by JspC, false otherwise
     */
    public ErrorDispatcher(boolean jspcMode) {
        // XXX check web.xml for custom error handler
        errHandler = new DefaultErrorHandler();
        this.jspcMode = jspcMode;
    }

    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     *
     * @throws JasperException An error occurred
     */
    public void jspError(String errCode, String... args) throws JasperException {
        dispatch(null, errCode, args, null);
    }

    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param where   Error location
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     *
     * @throws JasperException An error occurred
     */
    public void jspError(Mark where, String errCode, String... args) throws JasperException {
        dispatch(where, errCode, args, null);
    }

    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param n       Node that caused the error
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     *
     * @throws JasperException An error occurred
     */
    public void jspError(Node n, String errCode, String... args) throws JasperException {
        dispatch(n.getStart(), errCode, args, null);
    }

    /**
     * Dispatches the given parsing exception to the configured error handler.
     *
     * @param e Parsing exception
     *
     * @throws JasperException An error occurred
     */
    public void jspError(Exception e) throws JasperException {
        dispatch(null, null, null, e);
    }

    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     * @param e       Parsing exception
     *
     * @throws JasperException An error occurred
     */
    public void jspError(Exception e, String errCode, String... args) throws JasperException {
        dispatch(null, errCode, args, e);
    }

    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param where   Error location
     * @param e       Parsing exception
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     *
     * @throws JasperException An error occurred
     */
    public void jspError(Mark where, Exception e, String errCode, String... args) throws JasperException {
        dispatch(where, errCode, args, e);
    }

    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param n       Node that caused the error
     * @param e       Parsing exception
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     *
     * @throws JasperException An error occurred
     */
    public void jspError(Node n, Exception e, String errCode, String... args) throws JasperException {
        dispatch(n.getStart(), errCode, args, e);
    }

    /**
     * Parses the given error message into an array of javac compilation error messages (one per javac compilation error
     * line number).
     *
     * @param errMsg Error message
     * @param fname  Name of Java source file whose compilation failed
     * @param page   Node representation of JSP page from which the Java source file was generated
     *
     * @return Array of javac compilation errors, or null if the given error message does not contain any compilation
     *             error line numbers
     *
     * @throws JasperException An error occurred
     * @throws IOException     IO error which usually should not occur
     */
    public static JavacErrorDetail[] parseJavacErrors(String errMsg, String fname, Node.Nodes page)
            throws JasperException, IOException {

        return parseJavacMessage(errMsg, fname, page);
    }

    /**
     * Dispatches the given javac compilation errors to the configured error handler.
     *
     * @param javacErrors Array of javac compilation errors
     *
     * @throws JasperException An error occurred
     */
    public void javacError(JavacErrorDetail[] javacErrors) throws JasperException {

        errHandler.javacError(javacErrors);
    }


    /**
     * Dispatches the given compilation error report and exception to the configured error handler.
     *
     * @param errorReport Compilation error report
     * @param e           Compilation exception
     *
     * @throws JasperException An error occurred
     */
    public void javacError(String errorReport, Exception e) throws JasperException {

        errHandler.javacError(errorReport, e);
    }


    /**
     * Dispatches the given JSP parse error to the configured error handler. The given error code is localized. If it is
     * not found in the resource bundle for localized error messages, it is used as the error message.
     *
     * @param where   Error location
     * @param errCode Error code
     * @param args    Arguments for parametric replacement
     * @param e       Parsing exception
     *
     * @throws JasperException An error occurred
     */
    private void dispatch(Mark where, String errCode, Object[] args, Exception e) throws JasperException {
        String file = null;
        String errMsg = null;
        int line = -1;
        int column = -1;
        boolean hasLocation = false;

        // Localize
        if (errCode != null) {
            errMsg = Localizer.getMessage(errCode, args);
        } else if (e != null) {
            // give a hint about what's wrong
            errMsg = e.getMessage();
        }

        // Get error location
        if (where != null) {
            if (jspcMode) {
                // Get the full URL of the resource that caused the error
                try {
                    URL url = where.getURL();
                    if (url != null) {
                        file = url.toString();
                    } else {
                        // Fallback to using context-relative path
                        file = where.getFile();
                    }
                } catch (MalformedURLException me) {
                    // Fallback to using context-relative path
                    file = where.getFile();
                }
            } else {
                // Get the context-relative resource path, so as to not disclose any local file system details
                file = where.getFile();
            }
            line = where.getLineNumber();
            column = where.getColumnNumber();
            hasLocation = true;
        }

        // Get nested exception
        Exception nestedEx = e;
        if ((e instanceof SAXException) && (((SAXException) e).getException() != null)) {
            nestedEx = ((SAXException) e).getException();
        }

        if (hasLocation) {
            errHandler.jspError(file, line, column, errMsg, nestedEx);
        } else {
            errHandler.jspError(errMsg, nestedEx);
        }
    }

    /**
     * Parses the given Java compilation error message, which may contain one or more compilation errors, into an array
     * of JavacErrorDetail instances.
     * <p>
     * Each JavacErrorDetail instance contains the information about a single compilation error.
     *
     * @param errMsg Compilation error message that was generated by the javac compiler
     * @param fname  Name of Java source file whose compilation failed
     * @param page   Node representation of JSP page from which the Java source file was generated
     *
     * @return Array of JavacErrorDetail instances corresponding to the compilation errors
     *
     * @throws JasperException An error occurred
     * @throws IOException     IO error which usually should not occur
     */
    private static JavacErrorDetail[] parseJavacMessage(String errMsg, String fname, Node.Nodes page)
            throws IOException, JasperException {

        List<JavacErrorDetail> errors = new ArrayList<>();
        StringBuilder errMsgBuf = null;
        int lineNum = -1;
        JavacErrorDetail javacError = null;

        BufferedReader reader = new BufferedReader(new StringReader(errMsg));

        /*
         * Parse compilation errors. Each compilation error consists of a file path and error line number, followed by a
         * number of lines describing the error.
         */
        String line = null;
        while ((line = reader.readLine()) != null) {

            /*
             * Error line number is delimited by set of colons. Ignore colon following drive letter on Windows
             * (fromIndex = 2).
             *
             * XXX Handle deprecation warnings that don't have line info
             */
            int beginColon = line.indexOf(':', 2);
            int endColon = line.indexOf(':', beginColon + 1);
            if ((beginColon >= 0) && (endColon >= 0)) {
                if (javacError != null) {
                    // add previous error to error vector
                    errors.add(javacError);
                }

                String lineNumStr = line.substring(beginColon + 1, endColon);
                try {
                    lineNum = Integer.parseInt(lineNumStr);
                } catch (NumberFormatException e) {
                    lineNum = -1;
                }

                errMsgBuf = new StringBuilder();

                javacError = createJavacError(fname, page, errMsgBuf, lineNum);
            }

            // Ignore messages preceding first error
            if (errMsgBuf != null) {
                errMsgBuf.append(line);
                errMsgBuf.append(System.lineSeparator());
            }
        }

        // Add last error to error vector
        if (javacError != null) {
            errors.add(javacError);
        }

        reader.close();

        JavacErrorDetail[] errDetails = null;
        if (errors.size() > 0) {
            errDetails = errors.toArray(new JavacErrorDetail[0]);
        }

        return errDetails;
    }


    /**
     * Create a compilation error.
     *
     * @param fname     The file name
     * @param page      The page nodes
     * @param errMsgBuf The error message
     * @param lineNum   The source line number of the error
     *
     * @return JavacErrorDetail The error details
     *
     * @throws JasperException An error occurred
     */
    public static JavacErrorDetail createJavacError(String fname, Node.Nodes page, StringBuilder errMsgBuf, int lineNum)
            throws JasperException {
        return createJavacError(fname, page, errMsgBuf, lineNum, null);
    }


    /**
     * Create a compilation error.
     *
     * @param fname     The file name
     * @param page      The page nodes
     * @param errMsgBuf The error message
     * @param lineNum   The source line number of the error
     * @param ctxt      The compilation context
     *
     * @return JavacErrorDetail The error details
     *
     * @throws JasperException An error occurred
     */
    public static JavacErrorDetail createJavacError(String fname, Node.Nodes page, StringBuilder errMsgBuf, int lineNum,
            JspCompilationContext ctxt) throws JasperException {
        JavacErrorDetail javacError;
        // Attempt to map javac error line number to line in JSP page
        ErrorVisitor errVisitor = new ErrorVisitor(lineNum);
        page.visit(errVisitor);
        Node errNode = errVisitor.getJspSourceNode();
        if ((errNode != null) && (errNode.getStart() != null)) {
            // If this is a scriplet node then there is a one to one mapping between JSP lines and Java lines
            if (errVisitor.getJspSourceNode() instanceof Node.Scriptlet ||
                    errVisitor.getJspSourceNode() instanceof Node.Declaration) {
                javacError = new JavacErrorDetail(fname, lineNum, errNode.getStart().getFile(),
                        errNode.getStart().getLineNumber() + lineNum - errVisitor.getJspSourceNode().getBeginJavaLine(),
                        errMsgBuf, ctxt);
            } else {
                javacError = new JavacErrorDetail(fname, lineNum, errNode.getStart().getFile(),
                        errNode.getStart().getLineNumber(), errMsgBuf, ctxt);
            }
        } else {
            /*
             * javac error line number cannot be mapped to JSP page line number. For example, this is the case if a
             * scriptlet is missing a closing brace, which causes havoc with the try-catch-finally block that the code
             * generator places around all generated code: As a result of this, the javac error line numbers will be
             * outside the range of begin and end java line numbers that were generated for the scriptlet, and therefore
             * cannot be mapped to the start line number of the scriptlet in the JSP page.
             *
             * Include just the javac error info in the error detail.
             */
            javacError = new JavacErrorDetail(fname, lineNum, errMsgBuf);
        }
        return javacError;
    }


    /**
     * Visitor responsible for mapping a line number in the generated servlet source code to the corresponding JSP node.
     */
    private static class ErrorVisitor extends Node.Visitor {

        /**
         * Java source line number to be mapped
         */
        private final int lineNum;

        /**
         * JSP node whose Java source code range in the generated servlet contains the Java source line number to be
         * mapped
         */
        private Node found;

        /**
         * Constructor.
         *
         * @param lineNum Source line number in the generated servlet code
         */
        ErrorVisitor(int lineNum) {
            this.lineNum = lineNum;
        }

        @Override
        public void doVisit(Node n) throws JasperException {
            if ((lineNum >= n.getBeginJavaLine()) && (lineNum < n.getEndJavaLine())) {
                found = n;
            }
        }

        /**
         * Gets the JSP node to which the source line number in the generated servlet code was mapped.
         *
         * @return JSP node to which the source line number in the generated servlet code was mapped
         */
        public Node getJspSourceNode() {
            return found;
        }
    }
}