/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed 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.gradle.api.plugins.jetty;

import org.gradle.api.GradleException;
import org.gradle.api.internal.ConventionTask;
import org.gradle.api.plugins.jetty.internal.ConsoleScanner;
import org.gradle.api.plugins.jetty.internal.JettyPluginServer;
import org.gradle.api.plugins.jetty.internal.JettyPluginWebAppContext;
import org.gradle.api.plugins.jetty.internal.Monitor;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.gradle.internal.classpath.DefaultClassPath;
import org.gradle.internal.logging.progress.ProgressLogger;
import org.gradle.internal.logging.progress.ProgressLoggerFactory;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.RequestLog;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.security.UserRealm;
import org.mortbay.util.Scanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URLClassLoader;
import java.util.*;

/**
 * Base class for all tasks which deploy a web application to an embedded Jetty web container.
 */
public abstract class AbstractJettyRunTask extends ConventionTask {
    private final static Logger LOGGER = LoggerFactory.getLogger(AbstractJettyRunTask.class);

    private Iterable<File> additionalRuntimeJars = new ArrayList<File>();

    /**
     * The proxy for the Server object.
     */
    private JettyPluginServer server;

    /**
     * The "virtual" webapp created by the plugin.
     */
    private JettyPluginWebAppContext webAppConfig;

    /**
     * The context path for the webapp.
     */
    private String contextPath;

    /**
     * A webdefault.xml file to use instead of the default for the webapp. Optional.
     */
    private File webDefaultXml;

    /**
     * A web.xml file to be applied AFTER the webapp's web.xml file. Useful for applying different build profiles, eg test, production etc. Optional.
     */
    private File overrideWebXml;

    private int scanIntervalSeconds;

    protected String reload;

    /**
     * Location of a jetty XML configuration file whose contents will be applied before any plugin configuration. Optional.
     */
    private File jettyConfig;

    /**
     * Port to listen to stop jetty on.
     */
    private Integer stopPort;

    /**
     * Key to provide when stopping jetty.
     */
    private String stopKey;

    /**
     * <p> Determines whether or not the server blocks when started. The default behavior (daemon = false) will cause the server to pause other processes while it continues to handle web requests.
     * This is useful when starting the server with the intent to work with it interactively. </p><p> Often, it is desirable to let the server start and continue running subsequent processes in an
     * automated build environment. This can be facilitated by setting daemon to true. </p>
     */
    private boolean daemon;

    private Integer httpPort;

    /**
     * List of connectors to use. If none are configured then we use a single SelectChannelConnector at port 8080
     */
    private Connector[] connectors;

    /**
     * List of security realms to set up. Optional.
     */
    private UserRealm[] userRealms;

    /**
     * A RequestLog implementation to use for the webapp at runtime. Optional.
     */
    private RequestLog requestLog;

    /**
     * A scanner to check for changes to the webapp.
     */
    private Scanner scanner = new Scanner();

    /**
     * List of Listeners for the scanner.
     */
    protected List<Scanner.Listener> scannerListeners;

    /**
     * A scanner to check ENTER hits on the console.
     */
    protected Thread consoleScanner;

    public static final String PORT_SYSPROPERTY = "jetty.port";

    public abstract void validateConfiguration();

    public abstract void configureScanner();

    public abstract void applyJettyXml() throws Exception;

    /**
     * create a proxy that wraps a particular jetty version Server object.
     *
     * @return The Jetty Plugin Server
     */
    public abstract JettyPluginServer createServer() throws Exception;

    public abstract void finishConfigurationBeforeStart() throws Exception;

    @TaskAction
    protected void start() {
        ClassLoader originalClassloader = Server.class.getClassLoader();
        List<File> additionalClasspath = new ArrayList<File>();
        for (File additionalRuntimeJar : getAdditionalRuntimeJars()) {
            additionalClasspath.add(additionalRuntimeJar);
        }
        URLClassLoader jettyClassloader = new URLClassLoader(new DefaultClassPath(additionalClasspath).getAsURLArray(), originalClassloader);
        try {
            Thread.currentThread().setContextClassLoader(jettyClassloader);
            startJetty();
        } finally {
            Thread.currentThread().setContextClassLoader(originalClassloader);
        }
    }

    public JettyPluginServer getServer() {
        return this.server;
    }

    public void setServer(JettyPluginServer server) {
        this.server = server;
    }

    public void setScannerListeners(List<Scanner.Listener> listeners) {
        this.scannerListeners = new ArrayList<Scanner.Listener>(listeners);
    }

    public List<Scanner.Listener> getScannerListeners() {
        return this.scannerListeners;
    }

    public Scanner getScanner() {
        return scanner;
    }

    public void startJetty() {
        LOGGER.info("Configuring Jetty for " + getProject());
        validateConfiguration();
        startJettyInternal();
    }

    public void startJettyInternal() {
        ProgressLoggerFactory progressLoggerFactory = getServices().get(ProgressLoggerFactory.class);
        ProgressLogger progressLogger = progressLoggerFactory.newOperation(AbstractJettyRunTask.class)
                .start("Start Jetty server", "Starting Jetty");
        try {
            setServer(createServer());

            applyJettyXml();

            JettyPluginServer plugin = getServer();

            Object[] configuredConnectors = getConnectors();

            plugin.setConnectors(configuredConnectors);
            Object[] connectors = plugin.getConnectors();

            if (connectors == null || connectors.length == 0) {
                configuredConnectors = new Object[]{plugin.createDefaultConnector(getHttpPort())};
                plugin.setConnectors(configuredConnectors);
            }

            //set up a RequestLog if one is provided
            if (getRequestLog() != null) {
                getServer().setRequestLog(getRequestLog());
            }

            //set up the webapp and any context provided
            getServer().configureHandlers();
            configureWebApplication();
            getServer().addWebApplication(webAppConfig);

            // set up security realms
            Object[] configuredRealms = getUserRealms();
            for (int i = 0; (configuredRealms != null) && i < configuredRealms.length; i++) {
                LOGGER.debug(configuredRealms[i].getClass().getName() + ": " + configuredRealms[i].toString());
            }

            plugin.setUserRealms(configuredRealms);

            //do any other configuration required by the
            //particular Jetty version
            finishConfigurationBeforeStart();

            // start Jetty
            server.start();

            if (daemon) {
                return;
            }

            if (getStopPort() != null && getStopPort() > 0 && getStopKey() != null) {
                Monitor monitor = new Monitor(getStopPort(), getStopKey(), (Server) server.getProxiedObject());
                monitor.start();
            }

            // start the scanner thread (if necessary) on the main webapp
            configureScanner();
            startScanner();

            // start the new line scanner thread if necessary
            startConsoleScanner();

        } catch (Exception e) {
            throw new GradleException("Could not start the Jetty server.", e);
        } finally {
            progressLogger.completed();
        }

        progressLogger = progressLoggerFactory.newOperation(AbstractJettyRunTask.class)
                .start("Run Jetty at http://localhost:" + getHttpPort() + "/" + getContextPath(),
                       "Running at http://localhost:" + getHttpPort() + "/" + getContextPath());
        try {
            // keep the thread going if not in daemon mode
            server.join();
        } catch (Exception e) {
            throw new GradleException("Failed to wait for the Jetty server to stop.", e);
        } finally {
            progressLogger.completed();
        }
    }

    public abstract void restartWebApp(boolean reconfigureScanner) throws Exception;

    /**
     * Subclasses should invoke this to setup basic info on the webapp.
     */
    public void configureWebApplication() throws Exception {
        //use EITHER a <webAppConfig> element or the now deprecated <contextPath>, <webDefaultXml>, <overrideWebXml>
        //way of doing things
        if (webAppConfig == null) {
            webAppConfig = new JettyPluginWebAppContext();
        }
        webAppConfig.setContextPath(getContextPath().startsWith("/") ? getContextPath() : "/" + getContextPath());
        if (getTemporaryDir() != null) {
            webAppConfig.setTempDirectory(getTemporaryDir());
        }
        if (getWebDefaultXml() != null) {
            webAppConfig.setDefaultsDescriptor(getWebDefaultXml().getCanonicalPath());
        }
        if (getOverrideWebXml() != null) {
            webAppConfig.setOverrideDescriptor(getOverrideWebXml().getCanonicalPath());
        }

        // Don't treat JCL or Log4j as system classes
        Set<String> systemClasses = new LinkedHashSet<String>(Arrays.asList(webAppConfig.getSystemClasses()));
        systemClasses.remove("org.apache.commons.logging.");
        systemClasses.remove("org.apache.log4j.");
        webAppConfig.setSystemClasses(systemClasses.toArray(new String[0]));

        webAppConfig.setParentLoaderPriority(false);

        LOGGER.info("Context path = " + webAppConfig.getContextPath());
        LOGGER.info("Tmp directory = " + " determined at runtime");
        LOGGER.info("Web defaults = " + (webAppConfig.getDefaultsDescriptor() == null ? " jetty default"
                : webAppConfig.getDefaultsDescriptor()));
        LOGGER.info("Web overrides = " + (webAppConfig.getOverrideDescriptor() == null ? " none"
                : webAppConfig.getOverrideDescriptor()));
    }

    /**
     * Run a scanner thread on the given list of files and directories, calling stop/start on the given list of LifeCycle objects if any of the watched files change.
     */
    private void startScanner() {

        // check if scanning is enabled
        if (getScanIntervalSeconds() <= 0) {
            return;
        }

        // check if reload is manual. It disables file scanning
        if ("manual".equalsIgnoreCase(reload)) {
            // issue a warning if both scanIntervalSeconds and reload
            // are enabled
            LOGGER.warn("scanIntervalSeconds is set to " + scanIntervalSeconds
                    + " but will be IGNORED due to manual reloading");
            return;
        }

        scanner.setReportExistingFilesOnStartup(false);
        scanner.setScanInterval(getScanIntervalSeconds());
        scanner.setRecursive(true);
        List listeners = getScannerListeners();
        Iterator itor = listeners == null ? null : listeners.iterator();
        while (itor != null && itor.hasNext()) {
            scanner.addListener((Scanner.Listener) itor.next());
        }
        LOGGER.info("Starting scanner at interval of " + getScanIntervalSeconds() + " seconds.");
        scanner.start();
    }

    /**
     * Run a thread that monitors the console input to detect ENTER hits.
     */
    protected void startConsoleScanner() {
        if ("manual".equalsIgnoreCase(reload)) {
            LOGGER.info("Console reloading is ENABLED. Hit ENTER on the console to restart the context.");
            consoleScanner = new ConsoleScanner(this);
            consoleScanner.start();
        }
    }

    /**
     * Try and find a jetty-web.xml file, using some historical naming conventions if necessary.
     *
     * @return File object to the location of the jetty-web.xml
     */
    public File findJettyWebXmlFile(File webInfDir) {
        if (webInfDir == null) {
            return null;
        }
        if (!webInfDir.exists()) {
            return null;
        }

        File f = new File(webInfDir, "jetty-web.xml");
        if (f.exists()) {
            return f;
        }

        //try some historical alternatives
        f = new File(webInfDir, "web-jetty.xml");
        if (f.exists()) {
            return f;
        }
        f = new File(webInfDir, "jetty6-web.xml");
        if (f.exists()) {
            return f;
        }

        return null;
    }

    @InputFile
    @Optional
    public File getWebDefaultXml() {
        return webDefaultXml;
    }

    public void setWebDefaultXml(File webDefaultXml) {
        this.webDefaultXml = webDefaultXml;
    }

    @InputFile
    @Optional
    public File getOverrideWebXml() {
        return overrideWebXml;
    }

    public void setOverrideWebXml(File overrideWebXml) {
        this.overrideWebXml = overrideWebXml;
    }

    /**
     * Returns the interval in seconds between scanning the web app for file changes.
     * If file changes are detected, the web app is reloaded. Only relevant
     * if {@code reload} is set to {@code "automatic"}. Defaults to {@code 0},
     * which <em>disables</em> automatic reloading.
     */
    public int getScanIntervalSeconds() {
        return scanIntervalSeconds;
    }

    /**
     * Sets the interval in seconds between scanning the web app for file changes.
     * If file changes are detected, the web app is reloaded. Only relevant
     * if {@code reload} is set to {@code "automatic"}. Defaults to {@code 0},
     * which <em>disables</em> automatic reloading.
     */
    public void setScanIntervalSeconds(int scanIntervalSeconds) {
        this.scanIntervalSeconds = scanIntervalSeconds;
    }

    /**
     * Returns the context path to use to deploy the web application.
     */
    public String getContextPath() {
        return contextPath;
    }

    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    public JettyPluginWebAppContext getWebAppConfig() {
        return webAppConfig;
    }

    public void setWebAppConfig(JettyPluginWebAppContext webAppConfig) {
        this.webAppConfig = webAppConfig;
    }

    /**
     * Returns the reload mode, which is either {@code "automatic"} or {@code "manual"}.
     *
     * <p>In automatic mode, the web app is scanned for file changes every n seconds, where n is
     * determined by the {@code scanIntervalSeconds} property. (Note that {@code scanIntervalSeconds}
     * defaults to {@code 0}, which <em>disables</em> automatic reloading.) If files changes are
     * detected, the web app is reloaded.
     *
     * <p>In manual mode, the web app is reloaded whenever the Enter key is pressed.
     */
    public String getReload() {
        return reload;
    }

    /**
     * Sets the reload mode, which is either {@code "automatic"} or {@code "manual"}.
     *
     * <p>In automatic mode, the web app is scanned for file changes every n seconds, where n is
     * determined by the {@code scanIntervalSeconds} property. (Note that {@code scanIntervalSeconds}
     * defaults to {@code 0}, which <em>disables</em> automatic reloading.) If files changes are
     * detected, the web app is reloaded.
     *
     * <p>In manual mode, the web app is reloaded whenever the Enter key is pressed.
     */
    public void setReload(String reload) {
        this.reload = reload;
    }

    /**
     * Returns the jetty configuration file to use. When {@code null}, no configuration file is used.
     */
    @InputFile
    @Optional
    public File getJettyConfig() {
        return jettyConfig;
    }

    public void setJettyConfig(File jettyConfig) {
        this.jettyConfig = jettyConfig;
    }

    /**
     * Returns the TCP port for Jetty to listen on for stop requests.
     */
    public Integer getStopPort() {
        return stopPort;
    }

    public void setStopPort(Integer stopPort) {
        this.stopPort = stopPort;
    }

    /**
     * Returns the key to use to stop Jetty.
     */
    public String getStopKey() {
        return stopKey;
    }

    public void setStopKey(String stopKey) {
        this.stopKey = stopKey;
    }

    /**
     * Specifies whether the Jetty server should run in the background. When {@code true}, this task completes as soon as the server has started. When {@code false}, this task blocks until the Jetty
     * server is stopped.
     */
    public boolean isDaemon() {
        return daemon;
    }

    public void setDaemon(boolean daemon) {
        this.daemon = daemon;
    }

    /**
     * Returns the TCP port for Jetty to listen on for incoming HTTP requests.
     */
    public Integer getHttpPort() {
        return httpPort;
    }

    public void setHttpPort(Integer httpPort) {
        this.httpPort = httpPort;
    }

    public Connector[] getConnectors() {
        return connectors;
    }

    public void setConnectors(Connector[] connectors) {
        this.connectors = connectors;
    }

    public UserRealm[] getUserRealms() {
        return userRealms;
    }

    public void setUserRealms(UserRealm[] userRealms) {
        this.userRealms = userRealms;
    }

    public RequestLog getRequestLog() {
        return requestLog;
    }

    public void setRequestLog(RequestLog requestLog) {
        this.requestLog = requestLog;
    }

    /**
     * Returns the classpath to make available to the web application.
     */
    @InputFiles
    public Iterable<File> getAdditionalRuntimeJars() {
        return additionalRuntimeJars;
    }

    public void setAdditionalRuntimeJars(Iterable<File> additionalRuntimeJars) {
        this.additionalRuntimeJars = additionalRuntimeJars;
    }
}
