package org.apache.turbine;

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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.HomeDirectoryLocationStrategy;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.turbine.modules.PageLoader;
import org.apache.turbine.pipeline.Pipeline;
import org.apache.turbine.pipeline.PipelineData;
import org.apache.turbine.pipeline.TurbinePipeline;
import org.apache.turbine.services.Initable;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.ServiceManager;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.rundata.RunDataService;
import org.apache.turbine.services.template.TemplateService;
import org.apache.turbine.util.LocaleUtils;
import org.apache.turbine.util.RunData;
import org.apache.turbine.util.ServerData;
import org.apache.turbine.util.TurbineConfig;
import org.apache.turbine.util.TurbineException;
import org.apache.turbine.util.uri.URIConstants;

/**
 * <p>
 * Turbine is the main servlet for the entire system. If you need to perform
 * initialization of a service, then you should implement the Services API and
 * let your code be initialized by it.
 * </p>
 *
 * <p>
 * Turbine servlet recognizes the following initialization parameters.
 * </p>
 *
 * <ul>
 * <li><code>properties</code> the path to TurbineResources.properties file used
 * to configure Turbine, relative to the application root.</li>
 * <li><code>configuration</code> the path to TurbineConfiguration.xml file used
 * to configure Turbine from various sources, relative to the application
 * root.</li>
 * <li><code>applicationRoot</code> this parameter defaults to the web context
 * of the servlet container. You can use this parameter to specify the directory
 * within the server's filesystem, that is the base of your web
 * application.</li>
 * </ul>
 *
 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
 * @author <a href="mailto:bmclaugh@algx.net">Brett McLaughlin</a>
 * @author <a href="mailto:greg@shwoop.com">Greg Ritter</a>
 * @author <a href="mailto:john.mcnally@clearink.com">John D. McNally</a>
 * @author <a href="mailto:frank.kim@clearink.com">Frank Y. Kim</a>
 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
 * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
 * @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
 * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
 * @version $Id$
 */
@WebServlet(name = "Turbine", urlPatterns = { "/app" }, loadOnStartup = 1, initParams = {
        @WebInitParam(name = TurbineConstants.APPLICATION_ROOT_KEY, value = TurbineConstants.APPLICATION_ROOT_DEFAULT),
        @WebInitParam(name = TurbineConfig.PROPERTIES_PATH_KEY, value = TurbineConfig.PROPERTIES_PATH_DEFAULT) })
@MultipartConfig
public class Turbine extends HttpServlet
{
    /** Serial version */
    private static final long serialVersionUID = -6317118078613623990L;

    /**
     * Name of path info parameter used to indicate the redirected stage of a
     * given user's initial Turbine request
     *
     * @deprecated
     */
    @Deprecated // not used
    public static final String REDIRECTED_PATHINFO_NAME = "redirected";

    /**
     * The base directory key @deprecated
     */
    @Deprecated // not used
    public static final String BASEDIR_KEY = "basedir";

    /**
     * In certain situations the init() method is called more than once,
     * sometimes even concurrently. This causes bad things to happen, so we use
     * this flag to prevent it.
     */
    private static boolean firstInit = true;

    /**
     * The pipeline to use when processing requests.
     */
    private static Pipeline pipeline = null;

    /** Whether init succeeded or not. */
    private static Throwable initFailure = null;

    /**
     * Should initialization activities be performed during doGet() execution?
     */
    private static boolean firstDoGet = true;

    /**
     * Keep all the properties of the web server in a convenient data structure
     */
    private static volatile ServerData serverData = null;

    /** The base from which the Turbine application will operate. */
    private static String applicationRoot;

    /** Servlet config for this Turbine webapp. */
    private static ServletConfig servletConfig;

    /** Servlet context for this Turbine webapp. */
    private static ServletContext servletContext;

    /**
     * The webapp root where the Turbine application is running in the servlet
     * container. This might differ from the application root.
     */
    private static String webappRoot;

    /** Our internal configuration object */
    private static Configuration configuration = null;

    /** Which configuration method is being used */
    private enum ConfigurationStyle
    {
        XML, PROPERTIES, JSON, YAML, UNSET
    }

    private static final Logger log = LogManager.getLogger(Turbine.class);

    /**
     * This init method will load the default resources from a properties file.
     *
     * This method is called by init(ServletConfig config)
     *
     * @throws ServletException
     *             a servlet exception.
     */
    @Override
    public void init() throws ServletException
    {
        synchronized (Turbine.class)
        {
            super.init();

            if (!firstInit)
            {
                log.info("Double initialization of Turbine was attempted!");
                return;
            }
            // executing init will trigger some static initializers, so we have
            // only one chance.
            firstInit = false;
            ServletConfig config = getServletConfig();

            try
            {
                ServletContext context = config.getServletContext();

                configure(config, context);

                TemplateService templateService = (TemplateService) getServiceManager().getService(TemplateService.SERVICE_NAME);
                if (templateService == null)
                {
                    throw new TurbineException("No Template Service configured!");
                }

                if (getRunDataService() == null)
                {
                    throw new TurbineException("No RunData Service configured!");
                }
            }
            catch (Throwable e)
            {
                // save the exception to complain loudly later :-)
                initFailure = e;
                log.fatal("Turbine: init() failed", e);
                throw new ServletException("Turbine: init() failed", e);
            }

            log.info("Turbine: init() Ready to Rumble!");
        }
    }

    /**
     * Read the master configuration file in, configure logging and start up any
     * early services.
     *
     * @param config
     *            The Servlet Configuration supplied by the container
     * @param context
     *            The Servlet Context supplied by the container
     *
     * @throws Exception
     *             A problem occurred while reading the configuration or
     *             performing early startup
     */

    protected void configure(ServletConfig config, ServletContext context)
            throws Exception
    {

        // Set the application root. This defaults to the webapp
        // context if not otherwise set.
        applicationRoot = findInitParameter(context, config,
                TurbineConstants.APPLICATION_ROOT_KEY,
                TurbineConstants.APPLICATION_ROOT_DEFAULT);

        webappRoot = context.getRealPath("/");
        // log.info("Web Application root is " + webappRoot);
        // log.info("Application root is " + applicationRoot);

        if (applicationRoot == null || applicationRoot.equals(TurbineConstants.WEB_CONTEXT))
        {
            applicationRoot = webappRoot;
            // log.info("got empty or 'webContext' Application root. Application
            // root now: " + applicationRoot);
        }

        // Set the applicationRoot for this webapp.
        setApplicationRoot(applicationRoot);

        //
        // Now we run the Turbine configuration code. There are two ways
        // to configure Turbine:
        //
        // a) By supplying an web.xml init parameter called "configuration"
        //
        // <init-param>
        // <param-name>configuration</param-name>
        // <param-value>/WEB-INF/conf/turbine.xml</param-value>
        // </init-param>
        //
        // This loads an XML based configuration file.
        //
        // b) By supplying an web.xml init parameter called "properties"
        //
        // <init-param>
        // <param-name>properties</param-name>
        // <param-value>/WEB-INF/conf/TurbineResources.properties</param-value>
        // </init-param>
        //
        // This loads a Properties based configuration file. Actually, these are
        // extended properties as provided by commons-configuration
        //
        // If neither a) nor b) is supplied, Turbine will fall back to the
        // known behaviour of loading a properties file called
        // /WEB-INF/conf/TurbineResources.properties relative to the
        // web application root.

        Path confPath = configureApplication(config, context);

        configureLogging(confPath);

        //
        // Logging with log4j 2 is done via convention, finding in path

        setTurbineServletConfig(config);
        setTurbineServletContext(context);

        getServiceManager().setApplicationRoot(applicationRoot);

        // We want to set a few values in the configuration so
        // that ${variable} interpolation will work for
        //
        // ${applicationRoot}
        // ${webappRoot}
        configuration.setProperty(TurbineConstants.APPLICATION_ROOT_KEY, applicationRoot);
        configuration.setProperty(TurbineConstants.WEBAPP_ROOT_KEY, webappRoot);

        getServiceManager().setConfiguration(configuration);

        // Initialize the service manager. Services
        // that have its 'earlyInit' property set to
        // a value of 'true' will be started when
        // the service manager is initialized.
        getServiceManager().init();

        // Retrieve the pipeline class and then initialize it. The pipeline
        // handles the processing of a webrequest/response cycle.
        String descriptorPath = configuration.getString(
                "pipeline.default.descriptor",
                TurbinePipeline.CLASSIC_PIPELINE);

        log.debug("Using descriptor path: {}", descriptorPath);

        // context resource path has to begin with slash, cft.
        // context.getResource
        if (!descriptorPath.startsWith("/"))
        {
            descriptorPath = "/" + descriptorPath;
        }

        try (InputStream reader = context.getResourceAsStream(descriptorPath))
        {
            JAXBContext jaxb = JAXBContext.newInstance(TurbinePipeline.class);
            Unmarshaller unmarshaller = jaxb.createUnmarshaller();
            pipeline = (Pipeline) unmarshaller.unmarshal(reader);
        }

        log.debug("Initializing pipeline");

        pipeline.initialize();
    }

    /**
     * Checks configuraton style, resolves the location of the configuration and
     * loads it to internal {@link Configuration} object
     * ({@link #configuration}).
     *
     * Allows reading from a {@link CombinedConfigurationBuilder} xml configuration file.
     *
     * @param config
     *            the Servlet Configuration
     * @param context
     *            Servlet Context
     * @return The resolved Configuration Path
     * @throws IOException
     *             if configuration path not found
     * @throws ConfigurationException
     *             if failed to configure
     */
    protected Path configureApplication(ServletConfig config, ServletContext context)
            throws IOException, ConfigurationException
    {
        ConfigurationStyle confStyle = ConfigurationStyle.UNSET;
        // first test
        String confFile = findInitParameter(context, config,
                TurbineConfig.CONFIGURATION_PATH_KEY,
                null);
        if (StringUtils.isNotEmpty(confFile))
        {
            confStyle = ConfigurationStyle.XML;
        }
        else // second test
        {
            confFile = findInitParameter(context, config,
                    TurbineConfig.PROPERTIES_PATH_KEY,
                    null);
            if (StringUtils.isNotEmpty(confFile))
            {
                confStyle = ConfigurationStyle.PROPERTIES;
            }
        }
        // more tests ..
        // last test
        if (confStyle == ConfigurationStyle.UNSET)
        { // last resort
            confFile = findInitParameter(context, config,
                    TurbineConfig.PROPERTIES_PATH_KEY,
                    TurbineConfig.PROPERTIES_PATH_DEFAULT);
            confStyle = ConfigurationStyle.PROPERTIES;
        }

        // First report
        log.debug("Loading configuration ({}) from {}", confStyle, confFile);

        // now begin loading
        Parameters params = new Parameters();
        File confPath = getApplicationRootAsFile();

        if (confFile.startsWith("/"))
        {
            confFile = confFile.substring(1); // cft. RFC2396 should not start
                                              // with a slash, if not absolute
                                              // path
        }

        Path confFileRelativePath = Paths.get(confFile);// relative to later
                                                        // join
        Path targetPath = Paths.get(confPath.toURI());
        targetPath = targetPath.resolve(confFileRelativePath);

        // Get the target path directory
        Path targetPathDirectory = targetPath.getParent();
        if (targetPathDirectory != null)
        {
            // set the configuration path
            confPath = targetPathDirectory.normalize().toFile();

            Path targetFilePath = targetPath.getFileName();
            if (targetFilePath != null)
            {
                // set the configuration file name
                confFile = targetFilePath.toString();
            }

        }

        switch (confStyle)
        {
            case XML:
                // relative base path used for this and child configuration
                // files
                CombinedConfigurationBuilder combinedBuilder = new CombinedConfigurationBuilder()
                        .configure(params.fileBased()
                                .setFileName(confFile)
                                .setListDelimiterHandler(new DefaultListDelimiterHandler(','))
                                .setLocationStrategy(new HomeDirectoryLocationStrategy(confPath.getCanonicalPath(), false)));
                configuration = combinedBuilder.getConfiguration();
                break;

            case PROPERTIES:
                FileBasedConfigurationBuilder<PropertiesConfiguration> propertiesBuilder = new FileBasedConfigurationBuilder<>(
                        PropertiesConfiguration.class)
                                .configure(params.properties()
                                        .setFileName(confFile)
                                        .setListDelimiterHandler(new DefaultListDelimiterHandler(','))
                                        .setLocationStrategy(new HomeDirectoryLocationStrategy(confPath.getCanonicalPath(), false)));
                // meta configuration : this may contain any commons configuration: system<>, jndi, env
                configuration = propertiesBuilder.getConfiguration();
                break;
            case JSON:
            case YAML:
                throw new NotImplementedException("JSON or XAML configuration style not yet implemented!");

            default:
                break;
        }
        // Now report our successful configuration to the world
        log.info("Loaded configuration ({}) from {} style: {}",
                confStyle, confFile, configuration.toString());

        return targetPath;
    }

    /**
     * Finds the specified servlet configuration/initialization parameter,
     * looking first for a servlet-specific parameter, then for a global
     * parameter, and using the provided default if not found.
     *
     * @param context
     *            the servlet context
     * @param config
     *            configuration object
     * @param name
     *            name of parameter
     * @param defaultValue
     *            of the parameter
     * @return String value of the parameter
     */
    protected String findInitParameter(ServletContext context,
            ServletConfig config, String name, String defaultValue)
    {
        String path = null;
        String parameterName = name;

        // Try the name as provided first.
        boolean usingNamespace = parameterName.startsWith(TurbineConstants.CONFIG_NAMESPACE);
        while (true)
        {
            path = config.getInitParameter(parameterName);
            if (StringUtils.isEmpty(path))
            {
                path = context.getInitParameter(parameterName);
                if (StringUtils.isEmpty(path))
                {
                    // The named parameter didn't yield a value.
                    if (usingNamespace)
                    {
                        path = defaultValue;
                    }
                    else
                    {
                        // Try again using Turbine's namespace.
                        parameterName = TurbineConstants.CONFIG_NAMESPACE + '.' + parameterName;
                        usingNamespace = true;
                        continue;
                    }
                }
            }
            break;
        }

        return path;
    }

    /**
     * Initializes the services which need <code>PipelineData</code> to
     * initialize themselves (post startup).
     *
     * @param data
     *            The first <code>GET</code> request.
     */
    public void init(PipelineData data)
    {
        synchronized (Turbine.class)
        {
            if (firstDoGet)
            {
                // All we want to do here is save some servlet
                // information so that services and processes
                // that don't have direct access to a RunData
                // object can still know something about
                // the servlet environment.
                saveServletInfo(data);

                // Initialize services with the PipelineData instance
                TurbineServices services = (TurbineServices) getServiceManager();

                for (Iterator<String> i = services.getServiceNames(); i.hasNext();)
                {
                    String serviceName = i.next();
                    Object service = services.getService(serviceName);

                    if (service instanceof Initable)
                    {
                        try
                        {
                            ((Initable) service).init(data);
                        }
                        catch (InitializationException e)
                        {
                            log.warn("Could not initialize Initable {} with PipelineData", serviceName, e);
                        }
                    }
                }

                // Mark that we're done.
                firstDoGet = false;
                log.info("Turbine: first Request successful");
            }
        }
    }

    /**
     * Return the current configuration with all keys included
     *
     * @return a Configuration Object
     */
    public static Configuration getConfiguration()
    {
        return configuration;
    }

    /**
     * Return the server name.
     *
     * @return String server name
     */
    public static String getServerName()
    {
        return getDefaultServerData().getServerName();
    }

    /**
     * Return the server scheme.
     *
     * @return String server scheme
     */
    public static String getServerScheme()
    {
        return getDefaultServerData().getServerScheme();
    }

    /**
     * Return the server port.
     *
     * @return String server port
     */
    public static String getServerPort()
    {
        return Integer.toString(getDefaultServerData().getServerPort());
    }

    /**
     * Get the script name. This is the initial script name. Actually this is
     * probably not needed any more. I'll check. jvz.
     *
     * @return String initial script name.
     */
    public static String getScriptName()
    {
        return getDefaultServerData().getScriptName();
    }

    /**
     * Return the context path.
     *
     * @return String context path
     */
    public static String getContextPath()
    {
        return getDefaultServerData().getContextPath();
    }

    /**
     * Return all the Turbine Servlet information (Server Name, Port, Scheme in
     * a ServerData structure. This is generated from the values set when
     * initializing the Turbine and may not be correct if you're running in a
     * clustered structure. You can provide default values in your configuration
     * for cases where access is requied before your application is first
     * accessed by a user. This might be used if you need a DataURI and have no
     * RunData object handy.
     *
     * @return An initialized ServerData object
     */
    public static ServerData getDefaultServerData()
    {
        if (serverData == null)
        {
            String serverName = configuration.getString(TurbineConstants.DEFAULT_SERVER_NAME_KEY);
            if (serverName == null)
            {
                log.error("ServerData Information requested from Turbine before first request!");
            }
            else
            {
                log.info("ServerData Information retrieved from configuration.");
            }
            // Will be overwritten once the first request is run;
            serverData = new ServerData(serverName,
                    configuration.getInt(TurbineConstants.DEFAULT_SERVER_PORT_KEY,
                            URIConstants.HTTP_PORT),
                    configuration.getString(TurbineConstants.DEFAULT_SERVER_SCHEME_KEY,
                            URIConstants.HTTP),
                    configuration.getString(TurbineConstants.DEFAULT_SCRIPT_NAME_KEY),
                    configuration.getString(TurbineConstants.DEFAULT_CONTEXT_PATH_KEY));
        }
        return serverData;
    }

    /**
     * Set the servlet config for this turbine webapp.
     *
     * @param config
     *            New servlet config
     */
    public static void setTurbineServletConfig(ServletConfig config)
    {
        servletConfig = config;
    }

    /**
     * Get the servlet config for this turbine webapp.
     *
     * @return ServletConfig
     */
    public static ServletConfig getTurbineServletConfig()
    {
        return servletConfig;
    }

    /**
     * Set the servlet context for this turbine webapp.
     *
     * @param context
     *            New servlet context.
     */
    public static void setTurbineServletContext(ServletContext context)
    {
        servletContext = context;
    }

    /**
     * Get the servlet context for this turbine webapp.
     *
     * @return ServletContext
     */
    public static ServletContext getTurbineServletContext()
    {
        return servletContext;
    }

    /**
     * The <code>Servlet</code> destroy method. Invokes
     * <code>ServiceBroker</code> tear down method.
     */
    @Override
    public void destroy()
    {
        // Shut down all Turbine Services.
        getServiceManager().shutdownServices();

        firstInit = true;
        firstDoGet = true;
        log.info("Turbine: Done shutting down!");
    }

    /**
     * The primary method invoked when the Turbine servlet is executed.
     *
     * @param req
     *            Servlet request.
     * @param res
     *            Servlet response.
     * @throws IOException
     *             a servlet exception.
     * @throws ServletException
     *             a servlet exception.
     */
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException, ServletException
    {
        // Check to make sure that we started up properly.
        if (initFailure != null)
        {
            handleHorribleException(res, initFailure);
            return;
        }

        // Get general PipelineData here...
        try (PipelineData pipelineData = getRunDataService().getRunData(req, res, getServletConfig()))
        {
            try
            {
                // Perform turbine specific initialization below.
                Map<Class<?>, Object> runDataMap = new HashMap<>();
                runDataMap.put(RunData.class, pipelineData);
                // put the data into the pipeline
                pipelineData.put(RunData.class, runDataMap);

                // If this is the first invocation, perform some
                // initialization. Certain services need RunData to initialize
                // themselves.
                if (firstDoGet)
                {
                    init(pipelineData);
                }

                // Stages of Pipeline implementation execution
                // configurable via attached Valve implementations in a
                // XML properties file.
                pipeline.invoke(pipelineData);
            }
            catch (Throwable t)
            {
                handleException(pipelineData, res, t);
            }
        }
        catch (Throwable t)
        {
            handleHorribleException(res, t);
        }
    }

    /**
     * In this application doGet and doPost are the same thing.
     *
     * @param req
     *            Servlet request.
     * @param res
     *            Servlet response.
     * @throws IOException
     *             a servlet exception.
     * @throws ServletException
     *             a servlet exception.
     */
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse res)
            throws IOException, ServletException
    {
        doGet(req, res);
    }

    /**
     * Return the servlet info.
     *
     * @return a string with the servlet information.
     */
    @Override
    public String getServletInfo()
    {
        return "Turbine Servlet";
    }

    /**
     * This method is about making sure that we catch and display errors to the
     * screen in one fashion or another. What happens is that it will attempt to
     * show the error using your user defined Error Screen. If that fails, then
     * it will resort to just displaying the error and logging it all over the
     * place including the servlet engine log file, the Turbine log file and on
     * the screen.
     *
     * @param pipelineData
     *            A Turbine PipelineData object.
     * @param res
     *            Servlet response.
     * @param t
     *            The exception to report.
     */
    protected void handleException(PipelineData pipelineData, HttpServletResponse res,
            Throwable t)
    {
        RunData data = (RunData) pipelineData;
        // make sure that the stack trace makes it the log
        log.error("Turbine.handleException: ", t);

        try
        {
            // This is where we capture all exceptions and show the
            // Error Screen.
            data.setStackTrace(ExceptionUtils.getStackTrace(t), t);

            // setup the screen
            data.setScreen(configuration.getString(
                    TurbineConstants.SCREEN_ERROR_KEY,
                    TurbineConstants.SCREEN_ERROR_DEFAULT));

            // do more screen setup for template execution if needed
            if (data.getTemplateInfo() != null)
            {
                data.getTemplateInfo()
                        .setScreenTemplate(configuration.getString(
                                TurbineConstants.TEMPLATE_ERROR_KEY,
                                TurbineConstants.TEMPLATE_ERROR_VM));
            }

            // Make sure to not execute an action.
            data.setAction("");

            PageLoader.getInstance().exec(pipelineData,
                    configuration.getString(TurbineConstants.PAGE_DEFAULT_KEY,
                            TurbineConstants.PAGE_DEFAULT_DEFAULT));

            data.getResponse().setContentType(data.getContentType());
            data.getResponse().setStatus(data.getStatusCode());
        }
        // Attempt to do *something* at this point...
        catch (Throwable reallyScrewedNow)
        {
            handleHorribleException(res, reallyScrewedNow);
        }
    }

    /**
     * This method handles exception cases where no PipelineData object exists
     *
     * @param res
     *            Servlet response.
     * @param t
     *            The exception to report.
     */
    protected void handleHorribleException(HttpServletResponse res, Throwable t)
    {
        try
        {
            res.setContentType(TurbineConstants.DEFAULT_TEXT_CONTENT_TYPE);
            res.setStatus(200);
            PrintWriter writer = res.getWriter();
            writer.println("Horrible Exception: ");
            t.printStackTrace(writer);
        }
        catch (Exception ignored)
        {
            // ignore
        }

        log.error(t.getMessage(), t);
    }

    /**
     * Save some information about this servlet so that it can be utilized by
     * object instances that do not have direct access to PipelineData.
     *
     * @param data
     *            Turbine request data
     */
    public static synchronized void saveServletInfo(PipelineData data)
    {
        // Store the context path for tools like ContentURI and
        // the UIManager that use webapp context path information
        // for constructing URLs.

        //
        // Bundle all the information above up into a convenient structure
        //
        ServerData requestServerData = data.get(Turbine.class, ServerData.class);
        serverData = (ServerData) requestServerData.clone();
    }

    /**
     * Checks Log4j 2 Context, loads log4File, if configured and configuration
     * is not already located.
     *
     * @param logConf
     *            Configuration file path
     * @throws IOException
     *             if path not found
     */
    protected void configureLogging(Path logConf) throws IOException
    {
        LoggerContext context = (LoggerContext) LogManager.getContext(false);

        if (context.getConfiguration().getConfigurationSource().getLocation() == null)
        {
            Path log4jFile = resolveLog4j2(logConf.getParent());
            // configured + no other log4j configuration already found
            if (log4jFile != null)
            {
                LogManager.getContext(null, false, log4jFile.toUri());
            }
        }
        log.info("resolved log4j2 location: {}", context.getConfiguration().getConfigurationSource().getLocation());
    }

    /**
     * Check {@linkplain TurbineConstants#LOG4J2_CONFIG_FILE} in Turbine
     * configuration.
     *
     * @param logConfPath
     *            configuration directory
     * @return Resolved log4j2 {@link Path} or null, if not found or configured
     *         "none".
     */
    protected Path resolveLog4j2(Path logConfPath)
    {
        String log4jFile = configuration.getString(TurbineConstants.LOG4J2_CONFIG_FILE,
                TurbineConstants.LOG4J2_CONFIG_FILE_DEFAULT);

        if (log4jFile.startsWith("/"))
        {
            log4jFile = log4jFile.substring(1);
        }
        Path log4jTarget = null;
        if (StringUtils.isNotEmpty(log4jFile) && !log4jFile.equalsIgnoreCase("none"))
        {
            // log4j must either share path with configuration path or resolved
            // relatively

            if (logConfPath != null)
            {
                Path log4jFilePath = Paths.get(log4jFile);
                Path logFilePath = logConfPath.resolve(log4jFilePath);
                if (logFilePath != null && logFilePath.toFile().exists())
                {
                    log4jTarget = logFilePath.normalize();
                }
                else
                {
                    // fall back just using the filename, if path match
                    if (log4jFilePath != null && log4jFilePath.getParent() != null && logConfPath.endsWith(log4jFilePath.getParent()))
                    {
                        logFilePath = logConfPath.resolve(log4jFilePath.getFileName());
                        if (logFilePath != null && logFilePath.toFile().exists())
                        {
                            log4jTarget = logFilePath.normalize();
                        }
                    }
                }
            }
        }
        return log4jTarget;
    }

    /**
     * Set the application root for the webapp.
     *
     * @param val
     *            New app root.
     */
    public static void setApplicationRoot(String val)
    {
        applicationRoot = val;
    }

    /**
     * Get the application root for this Turbine webapp.
     *
     * @return String applicationRoot
     */
    public static String getApplicationRoot()
    {
        return applicationRoot;
    }

    /**
     * Get the application root for this Turbine webapp as a file object.
     *
     * @return File applicationRootFile
     */
    public static File getApplicationRootAsFile()
    {
        return new File(applicationRoot);
    }

    /**
     * Used to get the real path of configuration and resource information. This
     * can be used by an app being developed in a standard CVS layout.
     *
     * @param path
     *            path translated to the application root
     * @return the real path
     */
    public static String getRealPath(String path)
    {
        if (path.startsWith("/"))
        {
            return new File(getApplicationRootAsFile(), path.substring(1)).getAbsolutePath();
        }

        return new File(getApplicationRootAsFile(), path).getAbsolutePath();
    }

    /**
     * Return an instance of the currently configured Service Manager
     *
     * @return A service Manager instance
     */
    private ServiceManager getServiceManager()
    {
        return TurbineServices.getInstance();
    }

    /**
     * Returns the default input encoding for the servlet.
     *
     * @return the default input encoding.
     *
     * @deprecated Use
     *             {@link org.apache.turbine.pipeline.DefaultSetEncodingValve}
     *             to set default encoding
     */
    @Deprecated
    public static String getDefaultInputEncoding()
    {
        return LocaleUtils.getDefaultInputEncoding();
    }

    /**
     * Static Helper method for looking up the RunDataService
     *
     * @return A RunDataService
     */
    private RunDataService getRunDataService()
    {
        return (RunDataService) getServiceManager().getService(RunDataService.SERVICE_NAME);
    }
}
