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

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.jasper.Constants;
import org.apache.jasper.EmbeddedServletOptions;
import org.apache.jasper.Options;
import org.apache.jasper.compiler.JspRuntimeContext;
import org.apache.jasper.compiler.Localizer;
import org.apache.jasper.runtime.ExceptionUtils;
import org.apache.jasper.security.SecurityUtil;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.PeriodicEventListener;
import org.apache.tomcat.util.security.Escape;

/**
 * The Jasper JSP engine. The servlet container is responsible for providing a URLClassLoader for the web application
 * context Jasper is being used in. Jasper will try to get the Tomcat ServletContext attribute for its ServletContext
 * class loader, if that fails, it uses the parent class loader. In either case, it must be a URLClassLoader.
 */
public class JspServlet extends HttpServlet implements PeriodicEventListener {

    private static final long serialVersionUID = 1L;

    /**
     * Default constructor.
     */
    public JspServlet() {
    }

    // Logger
    private final transient Log log = LogFactory.getLog(JspServlet.class);

    private transient ServletContext context;
    /** The servlet configuration. */
    private ServletConfig config;
    private transient Options options;
    private transient JspRuntimeContext rctxt;
    /**
     * The JSP file for a JSP configured explicitly as a servlet, in environments where this
     * configuration is translated into an init-param for this servlet.
     */
    private String jspFile;


    @Override
    public void init(ServletConfig config) throws ServletException {

        super.init(config);
        this.config = config;
        this.context = config.getServletContext();

        // Initialize the JSP Runtime Context
        // Check for a custom Options implementation
        String engineOptionsName = config.getInitParameter("engineOptionsClass");
        if (Constants.IS_SECURITY_ENABLED && engineOptionsName != null) {
            log.info(Localizer.getMessage("jsp.info.ignoreSetting", "engineOptionsClass", engineOptionsName));
            engineOptionsName = null;
        }
        if (engineOptionsName != null) {
            // Instantiate the indicated Options implementation
            try {
                ClassLoader loader = Thread.currentThread().getContextClassLoader();
                Class<?> engineOptionsClass = loader.loadClass(engineOptionsName);
                Class<?>[] ctorSig = { ServletConfig.class, ServletContext.class };
                Constructor<?> ctor = engineOptionsClass.getConstructor(ctorSig);
                Object[] args = { config, context };
                options = (Options) ctor.newInstance(args);
            } catch (Throwable t) {
                Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(throwable);
                // Need to localize this.
                log.warn(Localizer.getMessage("jsp.warning.engineOptionsClass", engineOptionsName), throwable);
                // Use the default Options implementation
                options = new EmbeddedServletOptions(config, context);
            }
        } else {
            // Use the default Options implementation
            options = new EmbeddedServletOptions(config, context);
        }
        rctxt = new JspRuntimeContext(context, options);
        if (config.getInitParameter("jspFile") != null) {
            jspFile = config.getInitParameter("jspFile");
            try {
                if (null == context.getResource(jspFile)) {
                    return;
                }
            } catch (MalformedURLException e) {
                throw new ServletException(Localizer.getMessage("jsp.error.no.jsp", jspFile), e);
            }
            try {
                if (SecurityUtil.isPackageProtectionEnabled()) {
                    AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                        serviceJspFile(null, null, jspFile, true);
                        return null;
                    });
                } else {
                    serviceJspFile(null, null, jspFile, true);
                }
            } catch (IOException ioe) {
                throw new ServletException(Localizer.getMessage("jsp.error.precompilation", jspFile), ioe);
            } catch (PrivilegedActionException e) {
                Throwable t = e.getCause();
                if (t instanceof ServletException) {
                    throw (ServletException) t;
                }
                throw new ServletException(Localizer.getMessage("jsp.error.precompilation", jspFile), e);
            }
        }

        if (log.isDebugEnabled()) {
            log.debug(Localizer.getMessage("jsp.message.scratch.dir.is", options.getScratchDir().toString()));
            log.debug(Localizer.getMessage("jsp.message.dont.modify.servlets"));
        }
    }


    /**
     * Returns the number of JSPs for which JspServletWrappers exist, i.e., the number of JSPs that have been loaded
     * into the webapp with which this JspServlet is associated.
     * <p>
     * This info may be used for monitoring purposes.
     *
     * @return The number of JSPs that have been loaded into the webapp with which this JspServlet is associated
     */
    public int getJspCount() {
        return this.rctxt.getJspCount();
    }


    /**
     * Resets the JSP reload counter.
     *
     * @param count Value to which to reset the JSP reload counter
     */
    public void setJspReloadCount(int count) {
        this.rctxt.setJspReloadCount(count);
    }


    /**
     * Gets the number of JSPs that have been reloaded.
     * <p>
     * This info may be used for monitoring purposes.
     *
     * @return The number of JSPs (in the webapp with which this JspServlet is associated) that have been reloaded
     */
    public int getJspReloadCount() {
        return this.rctxt.getJspReloadCount();
    }


    /**
     * Gets the number of JSPs that are in the JSP limiter queue
     * <p>
     * This info may be used for monitoring purposes.
     *
     * @return The number of JSPs (in the webapp with which this JspServlet is associated) that are in the JSP limiter
     *             queue
     */
    public int getJspQueueLength() {
        return this.rctxt.getJspQueueLength();
    }


    /**
     * Gets the number of JSPs that have been unloaded.
     * <p>
     * This info may be used for monitoring purposes.
     *
     * @return The number of JSPs (in the webapp with which this JspServlet is associated) that have been unloaded
     */
    public int getJspUnloadCount() {
        return this.rctxt.getJspUnloadCount();
    }


    /**
     * <p>
     * Look for a <em>precompilation request</em> as described in Section 8.4.2 of the JSP 1.2 Specification.
     * <strong>WARNING</strong> - we cannot use <code>request.getParameter()</code> for this, because that will trigger
     * parsing all of the request parameters, and not give a servlet the opportunity to call
     * <code>request.setCharacterEncoding()</code> first.
     * </p>
     *
     * @param request The servlet request we are processing
     *
     * @exception ServletException if an invalid parameter value for the <code>jsp_precompile</code> parameter name is
     *                                 specified
     */
    boolean preCompile(HttpServletRequest request) throws ServletException {

        String queryString = request.getQueryString();
        if (queryString == null) {
            return false;
        }
        int start = queryString.indexOf(Constants.PRECOMPILE);
        if (start < 0) {
            return false;
        }
        queryString = queryString.substring(start + Constants.PRECOMPILE.length());
        if (queryString.length() == 0) {
            return true; // ?jsp_precompile
        }
        if (queryString.startsWith("&")) {
            return true; // ?jsp_precompile&foo=bar...
        }
        if (!queryString.startsWith("=")) {
            return false; // part of some other name or value
        }
        int limit = queryString.length();
        int ampersand = queryString.indexOf('&');
        if (ampersand > 0) {
            limit = ampersand;
        }
        String value = queryString.substring(1, limit);
        if (value.equals("true")) {
            return true; // ?jsp_precompile=true
        } else if (value.equals("false")) {
            // Spec says if jsp_precompile=false, the request should not
            // be delivered to the JSP page; the easiest way to implement
            // this is to set the flag to true, and precompile the page anyway.
            // This still conforms to the spec, since it says the
            // precompilation request can be ignored.
            return true; // ?jsp_precompile=false
        } else {
            throw new ServletException(
                    Localizer.getMessage("jsp.error.precompilation.parameter", Constants.PRECOMPILE, value));
        }

    }


    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // jspFile may be configured as an init-param for this servlet instance
        String jspUri = jspFile;

        if (jspUri == null) {
            /*
             * Check to see if the requested JSP has been the target of a RequestDispatcher.include()
             */
            jspUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
            if (jspUri != null) {
                /*
                 * Requested JSP has been target of RequestDispatcher.include(). Its path is assembled from the relevant
                 * javax.servlet.include.* request attributes
                 */
                String pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
                if (pathInfo != null) {
                    jspUri += pathInfo;
                }
            } else {
                /*
                 * Requested JSP has not been the target of a RequestDispatcher.include(). Reconstruct its path from the
                 * request's getServletPath() and getPathInfo()
                 */
                jspUri = request.getServletPath();
                String pathInfo = request.getPathInfo();
                if (pathInfo != null) {
                    jspUri += pathInfo;
                }
            }
        }

        if (log.isTraceEnabled()) {
            log.trace("JspEngine --> " + jspUri);
            log.trace("\t     ServletPath: " + request.getServletPath());
            log.trace("\t        PathInfo: " + request.getPathInfo());
            log.trace("\t        RealPath: " + context.getRealPath(jspUri));
            log.trace("\t      RequestURI: " + request.getRequestURI());
            log.trace("\t     QueryString: " + request.getQueryString());
        }

        try {
            boolean precompile = preCompile(request);
            serviceJspFile(request, response, jspUri, precompile);
        } catch (RuntimeException | IOException | ServletException e) {
            throw e;
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            throw new ServletException(t);
        }

    }

    @Override
    public void destroy() {
        if (log.isTraceEnabled()) {
            log.trace("JspServlet.destroy()");
        }

        rctxt.destroy();
    }


    @Override
    public void periodicEvent() {
        rctxt.checkUnload();
        rctxt.checkCompile();
    }

    // -------------------------------------------------------- Private Methods

    private void serviceJspFile(HttpServletRequest request, HttpServletResponse response, String jspUri,
            boolean precompile) throws ServletException, IOException {

        JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
        if (wrapper == null) {
            synchronized (this) {
                wrapper = rctxt.getWrapper(jspUri);
                if (wrapper == null) {
                    // Check if the requested JSP page exists, to avoid
                    // creating unnecessary directories and files.
                    if (null == context.getResource(jspUri)) {
                        handleMissingResource(request, response, jspUri);
                        return;
                    }
                    wrapper = new JspServletWrapper(config, options, jspUri, rctxt);
                    rctxt.addWrapper(jspUri, wrapper);
                }
            }
        }

        try {
            wrapper.service(request, response, precompile);
        } catch (FileNotFoundException fnfe) {
            handleMissingResource(request, response, jspUri);
        }

    }


    private void handleMissingResource(HttpServletRequest request, HttpServletResponse response, String jspUri)
            throws ServletException, IOException {

        String includeRequestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);

        String msg = Localizer.getMessage("jsp.error.file.not.found", jspUri);
        if (includeRequestUri != null) {
            // This file was included. Throw an exception as
            // a response.sendError() will be ignored
            // Strictly, filtering this is an application
            // responsibility but just in case...
            throw new ServletException(Escape.htmlElementContent(msg));
        } else {
            try {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
            } catch (IllegalStateException ise) {
                log.error(msg);
            }
        }
    }


}
