EsigateServer.java

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.ProtectionDomain;
import java.util.Properties;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.esigate.server.metrics.InstrumentedServerConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.codahale.metrics.jetty9.InstrumentedConnectionFactory;
import com.codahale.metrics.jetty9.InstrumentedHandler;
import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool;

/**
 * The bootstrap code for esigate-server, using jetty.
 * 
 * 
 * <p>
 * Inspiration from Ole Christian Rynning (http://open.bekk.no/embedded-jetty-7-webapp-executable-with-maven/)
 * 
 * @author Nicolas Richeton
 * 
 */
public final class EsigateServer {

    private static final Logger LOG = LoggerFactory.getLogger(EsigateServer.class);

    private static String contextPath;
    private static int controlPort;
    private static String extraClasspath;
    private static long idleTimeout = 0;
    private static int maxThreads = 0;
    private static int minThreads = 0;
    private static int outputBufferSize = 0;
    private static int port;
    private static String sessionCookieName;
    private static final int PROPERTY_DEFAULT_CONTROL_PORT = 8081;
    private static final int PROPERTY_DEFAULT_HTTP_PORT = 8080;
    private static final String PROPERTY_PREFIX = "server.";
    private static Server srv = null;

    private EsigateServer() {

    }

    /**
     * Get an integer from System properties
     * 
     * @param prefix
     * @param name
     * @param defaultValue
     * @return
     */
    private static int getProperty(String prefix, String name, int defaultValue) {
        int result = defaultValue;

        try {
            result = Integer.parseInt(System.getProperty(prefix + name));
        } catch (NumberFormatException e) {
            LOG.warn("Value for " + prefix + name + " must be an integer. Using default " + defaultValue);
        }
        return result;
    }

    /**
     * Get String from System properties
     * 
     * @param prefix
     * @param name
     * @param defaultValue
     * @return
     */
    private static String getProperty(String prefix, String name, String defaultValue) {
        return System.getProperty(prefix + name, defaultValue);
    }

    /**
     * Read server configuration from System properties and from server.properties.
     */
    public static void init() {

        // Get configuration

        // Read from "server.properties" or custom file.
        String configFile = null;
        Properties serverProperties = new Properties();
        try {
            configFile = System.getProperty(PROPERTY_PREFIX + "config", "server.properties");
            LOG.info("Loading server configuration from " + configFile);

            try (InputStream is = new FileInputStream(configFile)) {
                serverProperties.load(is);
            }

        } catch (FileNotFoundException e) {
            LOG.warn(configFile + " not found.");
        } catch (IOException e) {
            LOG.error("Unexpected error reading " + configFile);
        }

        init(serverProperties);
    }

    /**
     * Set the provided server configuration then read configuration from System properties or load defaults.
     * 
     * @param configuration
     *            configuration to use.
     */
    public static void init(Properties configuration) {

        for (Object prop : configuration.keySet()) {
            String serverPropertyName = (String) prop;
            System.setProperty(PROPERTY_PREFIX + serverPropertyName, configuration.getProperty(serverPropertyName));
        }

        // Read system properties
        LOG.info("Using configuration provided using '-D' parameter and/or default values");
        EsigateServer.port = getProperty(PROPERTY_PREFIX, "port", PROPERTY_DEFAULT_HTTP_PORT);
        EsigateServer.controlPort = getProperty(PROPERTY_PREFIX, "controlPort", PROPERTY_DEFAULT_CONTROL_PORT);
        EsigateServer.contextPath = getProperty(PROPERTY_PREFIX, "contextPath", "/");
        EsigateServer.extraClasspath = getProperty(PROPERTY_PREFIX, "extraClasspath", null);
        EsigateServer.maxThreads = getProperty(PROPERTY_PREFIX, "maxThreads", 500);
        EsigateServer.minThreads = getProperty(PROPERTY_PREFIX, "minThreads", 40);
        EsigateServer.outputBufferSize = getProperty(PROPERTY_PREFIX, "outputBufferSize", 8 * 1024);
        EsigateServer.idleTimeout = getProperty(PROPERTY_PREFIX, "idleTimeout", 30 * 1000);
        EsigateServer.sessionCookieName = getProperty(PROPERTY_PREFIX, "sessionCookieName", null);
    }

    /**
     * Returns current control port.
     * 
     * @return current control port.
     */
    public static int getControlPort() {
        return controlPort;
    }

    /**
     * Esigate Server entry point.
     * 
     * @param args
     *            command line arguments.
     * @throws Exception
     *             when server cannot be started.
     */
    public static void main(String[] args) throws Exception {

        if (args.length < 1) {
            EsigateServer.usage();
            return;
        }

        switch (args[0]) {
        case "start":
            EsigateServer.init();
            EsigateServer.start();
            break;

        case "stop":
            EsigateServer.stop();
            break;

        default:
            EsigateServer.usage();
            break;
        }
    }

    private static File resetTempDirectory(String currentDir) throws IOException {
        File workDir;
        // Currently disabled because this may be dangerous.
        // if (EsigateServer.workPath != null) {
        // workDir = new File(EsigateServer.workPath);
        // } else {
        workDir = new File(currentDir, "work");
        // }
        if (workDir.exists()) {
            try {
                FileUtils.cleanDirectory(workDir);
            } catch (IllegalArgumentException e) {
                // Strange behavior : if this directory exists, it disappears a
                // few ms later, causing this exception. We can ignore since we
                // initially wanted to delete it.
                LOG.info("Info: issue while deleting work directory, it was already deleted. Not a problem.");
            }
        }

        return workDir;

    }

    /**
     * Create and start server.
     * 
     * @throws Exception
     *             when server cannot be started.
     */
    public static void start() throws Exception {
        MetricRegistry registry = new MetricRegistry();

        QueuedThreadPool threadPool = new InstrumentedQueuedThreadPool(registry);
        threadPool.setName("esigate");
        threadPool.setMaxThreads(maxThreads);
        threadPool.setMinThreads(minThreads);

        srv = new Server(threadPool);
        srv.setStopAtShutdown(true);
        srv.setStopTimeout(5000);

        // HTTP Configuration
        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setOutputBufferSize(outputBufferSize);
        httpConfig.setSendServerVersion(false);
        Timer processTime = registry.timer("processTime");

        try (ServerConnector connector =
                new InstrumentedServerConnector("main", EsigateServer.port, srv, registry,
                        new InstrumentedConnectionFactory(new HttpConnectionFactory(httpConfig), processTime));
                ServerConnector controlConnector = new ServerConnector(srv)) {

            // Main connector
            connector.setIdleTimeout(EsigateServer.idleTimeout);
            connector.setSoLingerTime(-1);
            connector.setName("main");
            connector.setAcceptQueueSize(200);

            // Control connector
            controlConnector.setHost("127.0.0.1");
            controlConnector.setPort(EsigateServer.controlPort);
            controlConnector.setName("control");

            srv.setConnectors(new Connector[] {connector, controlConnector});
            // War
            ProtectionDomain protectionDomain = EsigateServer.class.getProtectionDomain();
            String warFile = protectionDomain.getCodeSource().getLocation().toExternalForm();
            String currentDir = new File(protectionDomain.getCodeSource().getLocation().getPath()).getParent();

            File workDir = resetTempDirectory(currentDir);

            WebAppContext context = new WebAppContext(warFile, EsigateServer.contextPath);
            context.setServer(srv);
            context.setTempDirectory(workDir);
            if (StringUtils.isNoneEmpty(sessionCookieName)) {
                context.getSessionHandler().setSessionCookie(sessionCookieName);
            }
            // Add extra classpath (allows to add extensions).
            if (EsigateServer.extraClasspath != null) {
                context.setExtraClasspath(EsigateServer.extraClasspath);
            }

            // Add the handlers
            HandlerCollection handlers = new HandlerList();
            // control handler must be the first one.
            // Work in progress, currently disabled.
            handlers.addHandler(new ControlHandler(registry));
            InstrumentedHandler ih = new InstrumentedHandler(registry);
            ih.setName("main");
            ih.setHandler(context);
            handlers.addHandler(ih);

            srv.setHandler(handlers);
            srv.start();
            srv.join();

        }

    }

    /**
     * Check if server is started.
     * 
     * @return true if started.
     */
    public static boolean isStarted() {
        return srv != null && srv.isStarted();
    }

    /**
     * Send a shutdown request to esigate server.
     */
    public static void stop() {
        ControlHandler.shutdown(EsigateServer.controlPort);
    }

    /**
     * Display usage information.
     */
    private static void usage() {
        StringBuilder usageText = new StringBuilder();
        usageText.append("Usage: java -D").append(PROPERTY_PREFIX)
                .append("config=esigate.properties -jar esigate-server.jar [start|stop]\n\t");
        usageText.append("start    Start the server (default)\n\t");
        usageText.append("stop     Stop the server gracefully\n\t");

        System.out.println(usageText.toString());
        System.exit(-1);
    }
}