org.tonguetied.server.Server.java Source code

Java tutorial

Introduction

Here is the source code for org.tonguetied.server.Server.java

Source

/*
 * Copyright 2008 The Tongue-Tied 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.tonguetied.server;

import static org.tonguetied.server.ServerConstants.DEFAULT_JOIN_THREAD_POOL;
import static org.tonguetied.server.ServerConstants.DEFAULT_KEYSTORE_PASSWORD;
import static org.tonguetied.server.ServerConstants.DEFAULT_MAX_THREADS;
import static org.tonguetied.server.ServerConstants.DEFAULT_MIN_THREADS;
import static org.tonguetied.server.ServerConstants.DEFAULT_PROP_FILE;
import static org.tonguetied.server.ServerConstants.DEFAULT_REQUEST_LOG_DAYS;
import static org.tonguetied.server.ServerConstants.DEFAULT_REQUEST_LOG_TIMEZONE;
import static org.tonguetied.server.ServerConstants.DEFAULT_SECURE_SERVER_PORT;
import static org.tonguetied.server.ServerConstants.DEFAULT_SERVER_PORT;
import static org.tonguetied.server.ServerConstants.DEFAULT_TEMP_DIR;
import static org.tonguetied.server.ServerConstants.DEFAULT_UNPACK_WAR;
import static org.tonguetied.server.ServerConstants.DEFAULT_WORKING_LOC;
import static org.tonguetied.server.ServerConstants.KEY_CONTEXT_PATH_DEF;
import static org.tonguetied.server.ServerConstants.KEY_JOIN_THREAD_POOL;
import static org.tonguetied.server.ServerConstants.KEY_KEYSTORE_LOC;
import static org.tonguetied.server.ServerConstants.KEY_KEYSTORE_PASSWORD;
import static org.tonguetied.server.ServerConstants.KEY_LOG_LEVEL;
import static org.tonguetied.server.ServerConstants.KEY_MAX_THREADS;
import static org.tonguetied.server.ServerConstants.KEY_MIN_THREADS;
import static org.tonguetied.server.ServerConstants.KEY_REQUEST_LOG_DAYS;
import static org.tonguetied.server.ServerConstants.KEY_REQUEST_LOG_FILE;
import static org.tonguetied.server.ServerConstants.KEY_REQUEST_TIMEZONE;
import static org.tonguetied.server.ServerConstants.KEY_SECURE_SERVER_PORT;
import static org.tonguetied.server.ServerConstants.KEY_SERVER_PORT;
import static org.tonguetied.server.ServerConstants.KEY_TEMP_DIR;
import static org.tonguetied.server.ServerConstants.KEY_UNPACK_WAR;
import static org.tonguetied.server.ServerConstants.KEY_USE_FILE_MAPPED_BUFFER;
import static org.tonguetied.server.ServerConstants.KEY_WORKING_LOC;
import static org.tonguetied.server.ServerConstants.USE_FILE_MAPPED_BUFFER;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.log4j.Logger;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.HandlerContainer;
import org.mortbay.jetty.NCSARequestLog;
import org.mortbay.jetty.RequestLog;
import org.mortbay.jetty.deployer.WebAppDeployer;
import org.mortbay.jetty.handler.ContextHandlerCollection;
import org.mortbay.jetty.handler.DefaultHandler;
import org.mortbay.jetty.handler.HandlerCollection;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.security.SslSocketConnector;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.thread.BoundedThreadPool;

/**
 * This class implements a simple HTTP Server using Jetty, providing a HTTP
 * listener and handle incoming requests. This class will start an embedded
 * jetty server initialized from a properties file. The default is
 * <code>embeddedServer.properties</code> or an alternative can be passed in
 * at runtime. Default values are derived from {@link ServerConstants}. By
 * default the server does not join the thread pool. To enable this behaviour
 * set the {@link ServerConstants#KEY_JOIN_THREAD_POOL} in the server properties
 * file.
 * 
 * The folling options are available for the command line:
 * <ul>
 * <li>-p &lt;propertyFileName&gt; the name and location of the properties file
 * to use.</li>
 * <li>-help, -? displays the help message.</li>
 * </ul>
 * 
 * @author mforslund
 * @see ServerConstants
 */
public class Server {
    private org.mortbay.jetty.Server embeddedServer;
    private boolean initialized;
    private Properties properties;
    private SslSocketConnector sslSocketConnector = new SslSocketConnector();

    private static String propsFile = DEFAULT_PROP_FILE;
    private static final String DATE_FORMAT = "yyyy-MM-dd";
    private static final long startTime = System.currentTimeMillis();

    /**
     * The list of classes used to configure the web application deployer.
     */
    static final String[] SERVER_CONFIGURATION_CLASSES = new String[] {
            org.mortbay.jetty.webapp.WebInfConfiguration.class.getCanonicalName(),
            org.mortbay.jetty.webapp.WebXmlConfiguration.class.getCanonicalName(),
            org.mortbay.jetty.webapp.JettyWebXmlConfiguration.class.getCanonicalName(),
            org.mortbay.jetty.webapp.TagLibConfiguration.class.getCanonicalName(),
            // "org.mortbay.jetty.plus.webapp.EnvConfiguration"
    };

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

    /**
     * Create a new instance of the embedded web server.
     * 
     * @param fileName the name and / or location of the properties file to load
     * @throws IOException if there is a problem reading the server properties
     *         file
     */
    public Server(final String fileName) throws IOException {
        properties = loadProperties(fileName);

        // Set debug level logging if specified
        if ("DEBUG".equalsIgnoreCase(getLogLevel())) {
            System.setProperty("DEBUG", "true");
        }

        if (log.isInfoEnabled())
            log.info("Creating new Server instance with properties:\n" + properties);
    }

    /**
     * This method returns diagnostics information from the server.
     * 
     * @return the server statistics
     */
    public static Map<String, String> getServerInfo() {
        Map<String, String> info = new HashMap<String, String>();
        info.put("serverVersion", org.mortbay.jetty.Server.getVersion());
        long uptime = System.currentTimeMillis() - startTime;
        info.put("uptime", Long.toString(uptime));

        return info;
    }

    /**
     * Configure the embedded jetty server. This method sets all the start up
     * parameters for the jetty server based on the values in the
     * <code>embeddedServer.properties</code>. If no values are found in this
     * file, then the default values are used.
     * 
     * @exception Exception if the embedded server fails to initialize
     * @see ServerConstants
     */
    protected void initializeServer() throws Exception {
        // Create the embeddedServer
        embeddedServer = new org.mortbay.jetty.Server();

        Connector connector = new SelectChannelConnector();
        connector.setPort(getPort());

        sslSocketConnector.setPort(getSecurePort());
        sslSocketConnector.setKeystore(getKeystoreLocation());
        sslSocketConnector.setKeyPassword(getKeystorePassword());
        sslSocketConnector.setPassword(getKeystorePassword());

        embeddedServer.addConnector(connector);
        embeddedServer.addConnector(sslSocketConnector);

        BoundedThreadPool threadPool = new BoundedThreadPool();
        threadPool.setMaxThreads(getMaxThreads());
        threadPool.setMinThreads(getMinThreads());
        embeddedServer.setThreadPool(threadPool);

        HandlerCollection handlers = new HandlerCollection();
        ContextHandlerCollection contexts = new ContextHandlerCollection();
        RequestLogHandler requestLogHandler = new RequestLogHandler();
        handlers.setHandlers(new Handler[] { contexts, new DefaultHandler(), requestLogHandler });

        embeddedServer.setHandler(handlers);

        requestLogHandler.setRequestLog(createRequestLog());

        WebAppDeployer deployer = createWebAppDeployer(contexts);
        // We need to start the deployer in order to create a WebAppContext
        deployer.start();

        configureWebAppContexts(deployer.getContexts());

        embeddedServer.addLifeCycle(deployer);
        embeddedServer.setStopAtShutdown(true);
        embeddedServer.setSendServerVersion(true);

        initialized = true;
    }

    /**
     * Factory method to create a new initialized instance of a
     * {@link WebAppDeployer}.
     * 
     * @param contexts the list of handlers to set for this
     *        {@link WebAppDeployer}
     * @return the newly created {@link WebAppDeployer}
     */
    private WebAppDeployer createWebAppDeployer(ContextHandlerCollection contexts) {
        WebAppDeployer deployer = new WebAppDeployer();
        deployer.setContexts(contexts);
        deployer.setWebAppDir(getWorkingLocation());
        deployer.setExtract(unpackWebArchive());
        deployer.setConfigurationClasses(SERVER_CONFIGURATION_CLASSES);
        deployer.setParentLoaderPriority(false);
        deployer.setAllowDuplicates(false);

        return deployer;
    }

    /**
     * Factory method to create a new initialized instance of a
     * {@link RequestLog}.
     * 
     * @return the newly created {@link RequestLog}
     */
    private RequestLog createRequestLog() {
        final DateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
        final String logDate = formatter.format(new Date());
        String logFile = properties.getProperty(KEY_REQUEST_LOG_FILE);
        logFile = logFile.replaceAll("X", logDate);

        NCSARequestLog requestLog = new NCSARequestLog(logFile);
        requestLog.setRetainDays(getRequestLogRetainDays());
        requestLog.setAppend(true);
        requestLog.setExtended(false);
        requestLog.setLogTimeZone(getRequestLogTimeZone());
        return requestLog;
    }

    /**
     * Configure the WebAppContext component of the embedded server. This method
     * does the following:
     * <ul>
     * <li>Set the context path of the url.</li>
     * <li>Set the temp directory where the server will work from.</li>
     * </ul>
     * 
     * If the temp directory is supposed to be extracted, create the directory
     * to which it is to be extracted. The reason is because a temporary 
     * directory will have its contents deleted when the webapp is stopped 
     * unless either it is called "work" or it pre-existed the deployment of 
     * the webapp.
     *
     * @param contexts the list of handlers for this web server
     * 
     * @see ServerConstants#KEY_CONTEXT_PATH_DEF
     * @see ServerConstants#KEY_TEMP_DIR
     * @see <a href="http://docs.codehaus.org/display/JETTY/Temporary+Directories">
     * Temporary Directories in Jetty</a>
     */
    private void configureWebAppContexts(HandlerContainer contexts) {
        String contextPathDef[] = getContextPathDef();

        WebAppContext context = (WebAppContext) contexts.getChildHandlerByClass(WebAppContext.class);
        final File tempDirectory = getTempDirectory();
        if (unpackWebArchive()) {
            if (tempDirectory.mkdir()) {
                if (log.isInfoEnabled())
                    log.info("creating new directory: " + tempDirectory);
            }
        }
        context.setTempDirectory(tempDirectory);

        Map<String, String> initParams = new HashMap<String, String>();
        // see http://docs.codehaus.org/display/JETTY/Files+locked+on+Windows
        // for reasons on why we do this
        if (useFileMappedBuffer() != null)
            initParams.put(USE_FILE_MAPPED_BUFFER, useFileMappedBuffer());
        context.setInitParams(initParams);

        if (contextPathDef != null) {
            // This is one way of doing it, another way would be to use another
            // request forwarding solution like Apache but that would be a but
            // overkill...
            if (contextPathDef[0].equals(context.getContextPath())) {
                context.setContextPath(contextPathDef[1]);
            }
        }
    }

    private String[] getContextPathDef() {
        final String contextPathDef = properties.getProperty(KEY_CONTEXT_PATH_DEF);
        return contextPathDef == null ? null : contextPathDef.split(",");
    }

    private int getPort() {
        return Integer.parseInt(properties.getProperty(KEY_SERVER_PORT, DEFAULT_SERVER_PORT));
    }

    private int getSecurePort() {
        return Integer.parseInt(properties.getProperty(KEY_SECURE_SERVER_PORT, DEFAULT_SECURE_SERVER_PORT));
    }

    private String getKeystorePassword() {
        return properties.getProperty(KEY_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
    }

    private String getWorkingLocation() {
        return properties.getProperty(KEY_WORKING_LOC, DEFAULT_WORKING_LOC);
    }

    private File getTempDirectory() {
        final String tempDirStr = properties.getProperty(KEY_TEMP_DIR);
        final File tempDir = (tempDirStr == null || "".equals(tempDirStr)) ? DEFAULT_TEMP_DIR
                : new File(tempDirStr);

        return tempDir;
    }

    private String getLogLevel() {
        return properties.getProperty(KEY_LOG_LEVEL);
    }

    private String getKeystoreLocation() {
        return properties.getProperty(KEY_KEYSTORE_LOC, sslSocketConnector.getKeystore());
    }

    private int getRequestLogRetainDays() {
        return Integer.parseInt(properties.getProperty(KEY_REQUEST_LOG_DAYS, DEFAULT_REQUEST_LOG_DAYS));
    }

    private String getRequestLogTimeZone() {
        return properties.getProperty(KEY_REQUEST_TIMEZONE, DEFAULT_REQUEST_LOG_TIMEZONE);
    }

    private boolean isJoiningThreadPool() {
        return Boolean.parseBoolean(properties.getProperty(KEY_JOIN_THREAD_POOL, DEFAULT_JOIN_THREAD_POOL));
    }

    private boolean unpackWebArchive() {
        return Boolean.parseBoolean(properties.getProperty(KEY_UNPACK_WAR, DEFAULT_UNPACK_WAR));
    }

    private int getMaxThreads() {
        return Integer.parseInt(properties.getProperty(KEY_MAX_THREADS, DEFAULT_MAX_THREADS));
    }

    private int getMinThreads() {
        return Integer.parseInt(properties.getProperty(KEY_MIN_THREADS, DEFAULT_MIN_THREADS));
    }

    private String useFileMappedBuffer() {
        return properties.getProperty(KEY_USE_FILE_MAPPED_BUFFER);
    }

    /**
     * Starts the embedded Jetty server.
     * 
     * @throws Exception if the embedded server has not been initialized
     */
    protected void startServer() throws Exception {
        if (!initialized)
            throw new RuntimeException("Server not initialized, please initialize!!");

        embeddedServer.start();
        // Only join the thread pool if the user explicitly states
        if (isJoiningThreadPool())
            embeddedServer.join();

        if (log.isInfoEnabled())
            log.info("Server started, have fun!!");
    }

    public void start() {
        if (embeddedServer == null || !embeddedServer.isRunning()) {
            try {
                initializeServer();
                startServer();
            } catch (Exception e) {
                log.error("Cannot start server, please check config!!", e);
                System.exit(-1);
            }
        } else {
            log.warn("server is already running. Cannot start new instance" + " of same server.");
        }
    }

    /**
     * Stops the HTTP server and terminates the thread.
     */
    public void shutdown() {
        try {
            embeddedServer.stop();
        } catch (Exception e) {
            log.error("Error when stopping the server!!", e);
            System.exit(-1);
        }
    }

    /**
     * Entry point for the embedded server. Starts the embedded web server,
     * deploying the war file. The server settings are configured in the
     * <code>embeddedServer.properties</code> by default. If another
     * properties file is to be used specify the location as an argument with
     * the <code>-p</code> option.
     * 
     * @param args the list of arguments to start the application
     */
    public static void main(String[] args) throws Exception {
        readArguments(args);

        Server server = new Server(propsFile);
        server.start();
    }

    /**
     * Read and process command line arguments when the server is run.
     * 
     * @param args the arguments to process
     * @throws IllegalArgumentException if the arguments fail to be initialized
     *         correctly
     */
    private static void readArguments(String[] args) throws IllegalArgumentException {
        final Option help = new Option("help", "print this message");
        OptionBuilder.withArgName("file");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("the name and location of the properties file to use");
        final Option propertiesFile = OptionBuilder.create("p");

        Options options = new Options();
        options.addOption(help);
        options.addOption(propertiesFile);

        CommandLineParser parser = new GnuParser();
        try {
            // parse the command line arguments
            CommandLine line = parser.parse(options, args);

            // has the properties file argument been passed?
            if (line.hasOption("p")) {
                // initialise the member variable
                propsFile = line.getOptionValue("p");
            }
            if (line.hasOption("help")) {
                printHelp(options, 0);
            }
        } catch (ParseException pe) {
            // oops, something went wrong
            System.err.println("Parsing failed. Reason: " + pe.getMessage());
            printHelp(options, 1);
        }
    }

    /**
     * Print the help message and exit the system.
     * 
     * @param options used to create the help message.
     * @param status exit status
     * 
     * @see System#exit(int)
     */
    private static void printHelp(Options options, int status) {
        // automatically generate the help statement
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("Server", options, true);
        System.exit(status);
    }

    /**
     * Load the contents of a resource file into a {@link Properties} object.
     * This method assumes the resource file is in the same class path as this
     * {@linkplain Server} object. The order of execution for locating the file
     * is:
     * <ul>
     * <li>Attempt to load the file from the classpath</li>
     * <li>if no file matching name is found, then attempt load the file from
     * the file system.</li>
     * </ul>
     * 
     * @param fileName the name of resource file.
     * @return the resources loaded from the file.
     * @throws IOException if an error occurs reading the file.
     * @throws IllegalArgumentException if <code>fileName</code> is
     *         <code>null</code>
     */
    private static Properties loadProperties(String fileName) throws IOException {
        if (fileName == null) {
            throw new IllegalArgumentException("resource name cannot be null");
        }
        Properties props = new Properties();
        InputStream is = null;
        BufferedInputStream bis = null;
        try {
            // static reference to class. if use of this.getClass may result in
            // problems if a class from another package extends this class
            is = Server.class.getClassLoader().getResourceAsStream(fileName);
            if (is == null) {
                File file = new File(fileName);
                is = new FileInputStream(file);
            }
            bis = new BufferedInputStream(is);
            props.load(bis);
        } finally {
            if (bis != null)
                bis.close();
            if (is != null)
                is.close();
        }

        return props;
    }
}