com.google.gwt.dev.shell.tomcat.EmbeddedTomcatServer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dev.shell.tomcat.EmbeddedTomcatServer.java

Source

/*
 * Copyright 2006 Google Inc.
 * 
 * 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 com.google.gwt.dev.shell.tomcat;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.resource.impl.ClassPathEntry;
import com.google.gwt.dev.resource.impl.PathPrefix;
import com.google.gwt.dev.resource.impl.PathPrefixSet;
import com.google.gwt.dev.resource.impl.ResourceOracleImpl;
import com.google.gwt.dev.shell.WorkDirs;
import com.google.gwt.dev.util.Util;

import org.apache.catalina.Connector;
import org.apache.catalina.ContainerEvent;
import org.apache.catalina.ContainerListener;
import org.apache.catalina.Engine;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Logger;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Embedded;
import org.apache.catalina.startup.HostConfig;
import org.apache.coyote.tomcat5.CoyoteConnector;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Wraps an instance of the Tomcat web server used in hosted mode.
 */
public class EmbeddedTomcatServer {

    static EmbeddedTomcatServer sTomcat;

    public static int getPort() {
        return sTomcat.port;
    }

    public static String start(TreeLogger topLogger, int port, WorkDirs workDirs) {
        return start(topLogger, port, workDirs, true);
    }

    public static synchronized String start(TreeLogger topLogger, int port, WorkDirs workDirs,
            boolean shouldAutoGenerateResources) {
        if (sTomcat != null) {
            throw new IllegalStateException("Embedded Tomcat is already running");
        }

        try {
            new EmbeddedTomcatServer(topLogger, port, workDirs, shouldAutoGenerateResources);
            return null;
        } catch (LifecycleException e) {
            String msg = e.getMessage();
            if (msg != null && msg.indexOf("already in use") != -1) {
                msg = "Port " + port + " is already is use; you probably still have another session active";
            } else {
                msg = "Unable to start the embedded Tomcat server; double-check that your configuration is valid";
            }
            return msg;
        }
    }

    // Stop the embedded Tomcat server.
    //
    public static synchronized void stop() {
        if (sTomcat != null) {
            try {
                sTomcat.catEmbedded.stop();
            } catch (LifecycleException e) {
                // There's nothing we can really do about this and the logger is
                // gone in many scenarios, so we just ignore it.
                //
            } finally {
                sTomcat = null;
            }
        }
    }

    /**
     * Returns what local port the Tomcat connector is running on.
     * 
     * When starting Tomcat with port 0 (i.e. choose an open port), there is just
     * no way to figure out what port it actually chose. So we're using pure
     * hackery to steal the port via reflection. The only works because we bundle
     * Tomcat with GWT and know exactly what version it is.
     */
    private static int computeLocalPort(Connector connector) {
        Throwable caught = null;
        try {
            Field phField = CoyoteConnector.class.getDeclaredField("protocolHandler");
            phField.setAccessible(true);
            Object protocolHandler = phField.get(connector);

            Field epField = protocolHandler.getClass().getDeclaredField("ep");
            epField.setAccessible(true);
            Object endPoint = epField.get(protocolHandler);

            Field ssField = endPoint.getClass().getDeclaredField("serverSocket");
            ssField.setAccessible(true);
            ServerSocket serverSocket = (ServerSocket) ssField.get(endPoint);

            return serverSocket.getLocalPort();
        } catch (SecurityException e) {
            caught = e;
        } catch (NoSuchFieldException e) {
            caught = e;
        } catch (IllegalArgumentException e) {
            caught = e;
        } catch (IllegalAccessException e) {
            caught = e;
        }
        throw new RuntimeException("Failed to retrieve the startup port from Embedded Tomcat", caught);
    }

    private Embedded catEmbedded;

    private Engine catEngine;

    private StandardHost catHost = null;

    private int port;

    private final TreeLogger startupBranchLogger;

    private EmbeddedTomcatServer(final TreeLogger topLogger, int listeningPort, final WorkDirs workDirs,
            final boolean shouldAutoGenerateResources) throws LifecycleException {
        if (topLogger == null) {
            throw new NullPointerException("No logger specified");
        }

        final TreeLogger logger = topLogger.branch(TreeLogger.INFO, "Starting HTTP on port " + listeningPort, null);

        startupBranchLogger = logger;

        // Make myself the one static instance.
        // NOTE: there is only one small implementation reason that this has
        // to be a singleton, which is that the commons logger LogFactory insists
        // on creating your logger class which must have a constructor with
        // exactly one String argument, and since we want LoggerAdapter to delegate
        // to the logger instance available through instance host, there is no
        // way I can think of to delegate without accessing a static field.
        // An inner class is almost right, except there's no outer instance.
        //
        sTomcat = this;

        // Assume the working directory is simply the user's current directory.
        //
        File topWorkDir = new File(System.getProperty("user.dir"));

        // Tell Tomcat its base directory so that it won't complain.
        //
        String catBase = System.getProperty("catalina.base");
        if (catBase == null) {
            // we (briefly) supported catalina.base.create, so let's not cut support
            // until the deprecated sunset
            catBase = System.getProperty("catalina.base.create");
            if (catBase != null) {
                logger.log(TreeLogger.WARN, "catalina.base.create is deprecated.  "
                        + "Use catalina.base, and it will be created if necessary.");
                topWorkDir = new File(catBase);
            }
            catBase = generateDefaultCatalinaBase(logger, topWorkDir);
            System.setProperty("catalina.base", catBase);
        }

        // Some debug messages for ourselves.
        //
        if (logger.isLoggable(TreeLogger.DEBUG)) {
            logger.log(TreeLogger.DEBUG, "catalina.base = " + catBase, null);
        }

        // Set up the logger that will be returned by the Commons logging factory.
        //
        String adapterClassName = CommonsLoggerAdapter.class.getName();
        System.setProperty("org.apache.commons.logging.Log", adapterClassName);

        // And set up an adapter that will work with the Catalina logger family.
        //
        Logger catalinaLogger = new CatalinaLoggerAdapter(topLogger);

        // Create an embedded server.
        //
        catEmbedded = new Embedded();
        catEmbedded.setDebug(0);
        catEmbedded.setLogger(catalinaLogger);

        // The embedded engine is called "gwt".
        //
        catEngine = catEmbedded.createEngine();
        catEngine.setName("gwt");
        catEngine.setDefaultHost("localhost");
        catEngine.setParentClassLoader(this.getClass().getClassLoader());

        // It answers localhost requests.
        //
        // String appBase = fCatalinaBaseDir.getAbsolutePath();
        String appBase = catBase + "/webapps";
        catHost = (StandardHost) catEmbedded.createHost("localhost", appBase);

        // Hook up a host config to search for and pull in webapps.
        //
        HostConfig hostConfig = new HostConfig();
        catHost.addLifecycleListener(hostConfig);

        // Hook pre-install events so that we can add attributes to allow loaded
        // instances to find their development instance host.
        //
        catHost.addContainerListener(new ContainerListener() {
            public void containerEvent(ContainerEvent event) {
                if (StandardHost.PRE_INSTALL_EVENT.equals(event.getType())) {
                    StandardContext webapp = (StandardContext) event.getData();
                    publishShellLoggerAttribute(logger, topLogger, webapp);
                    publishShellWorkDirsAttribute(logger, workDirs, webapp);
                    publishShouldAutoGenerateResourcesAttribute(logger, shouldAutoGenerateResources, webapp);
                }
            }
        });

        // Tell the engine about the host.
        //
        catEngine.addChild(catHost);
        catEngine.setDefaultHost(catHost.getName());

        // Tell the embedded manager about the engine.
        //
        catEmbedded.addEngine(catEngine);
        InetAddress nullAddr = null;
        Connector connector = catEmbedded.createConnector(nullAddr, listeningPort, false);
        catEmbedded.addConnector(connector);

        // start up!
        catEmbedded.start();
        port = computeLocalPort(connector);

        if (port != listeningPort) {
            if (logger.isLoggable(TreeLogger.INFO)) {
                logger.log(TreeLogger.INFO, "HTTP listening on port " + port, null);
            }
        }
    }

    public TreeLogger getLogger() {
        return startupBranchLogger;
    }

    /*
     * Assumes that the leaf is a file (not a directory).
     */
    private void copyFileNoOverwrite(TreeLogger logger, String srcResName, Resource srcRes, File catBase) {

        File dest = new File(catBase, srcResName);
        try {
            // Only copy if src is newer than desc.
            long srcLastModified = srcRes.getLastModified();
            long dstLastModified = dest.lastModified();

            if (srcLastModified < dstLastModified) {
                // Don't copy over it.
                if (logger.isLoggable(TreeLogger.SPAM)) {
                    logger.log(TreeLogger.SPAM, "Source is older than existing: " + dest.getAbsolutePath(), null);
                }
                return;
            } else if (srcLastModified == dstLastModified) {
                // Exact same time; quietly don't overwrite.
                return;
            } else if (dest.exists()) {
                // Warn about the overwrite
                logger.log(TreeLogger.WARN, "Overwriting existing file '" + dest.getAbsolutePath() + "' with '"
                        + srcRes.getLocation() + "', which has a newer timestamp");
            }

            // Make dest directories as required.
            File destParent = dest.getParentFile();
            if (destParent != null) {
                // No need to check mkdirs result because IOException later anyway.
                destParent.mkdirs();
            }

            Util.copy(srcRes.openContents(), new FileOutputStream(dest));
            dest.setLastModified(srcLastModified);

            if (logger.isLoggable(TreeLogger.TRACE)) {
                logger.log(TreeLogger.TRACE, "Wrote: " + dest.getAbsolutePath(), null);
            }
        } catch (IOException e) {
            logger.log(TreeLogger.WARN, "Failed to write: " + dest.getAbsolutePath(), e);
        }
    }

    /**
     * Extracts a valid catalina base instance from the classpath. Does not
     * overwrite any existing files.
     */
    private String generateDefaultCatalinaBase(TreeLogger logger, File workDir) {
        logger = logger.branch(TreeLogger.TRACE,
                "Property 'catalina.base' not specified; checking for a standard catalina base image instead",
                null);

        // Recursively copies out files and directories
        String tomcatEtcDir = "com/google/gwt/dev/etc/tomcat/";
        Map<String, Resource> resourceMap = null;
        Throwable caught = null;
        try {
            resourceMap = getResourcesFor(logger, tomcatEtcDir);
        } catch (URISyntaxException e) {
            caught = e;
        } catch (IOException e) {
            caught = e;
        }

        File catBase = new File(workDir, "tomcat");
        if (resourceMap == null || resourceMap.isEmpty()) {
            logger.log(TreeLogger.WARN, "Could not find " + tomcatEtcDir, caught);
        } else {
            for (Entry<String, Resource> entry : resourceMap.entrySet()) {
                copyFileNoOverwrite(logger, entry.getKey(), entry.getValue(), catBase);
            }
        }

        return catBase.getAbsolutePath();
    }

    /**
     * Hacky, but fast.
     */
    private Map<String, Resource> getResourcesFor(TreeLogger logger, String tomcatEtcDir)
            throws URISyntaxException, IOException {
        ClassLoader contextClassLoader = this.getClass().getClassLoader();
        URL url = contextClassLoader.getResource(tomcatEtcDir);
        if (url == null) {
            return null;
        }
        String prefix = "";
        String urlString = url.toString();
        if (urlString.startsWith("jar:")) {
            assert urlString.toLowerCase(Locale.ENGLISH).contains(".jar!/" + tomcatEtcDir);
            urlString = urlString.substring(4, urlString.indexOf('!'));
            url = new URL(urlString);
            prefix = tomcatEtcDir;
        } else if (urlString.startsWith("zip:")) {
            assert urlString.toLowerCase(Locale.ENGLISH).contains(".zip!/" + tomcatEtcDir);
            urlString = urlString.substring(4, urlString.indexOf('!'));
            url = new URL(urlString);
            prefix = tomcatEtcDir;
        }
        ClassPathEntry entry = ResourceOracleImpl.createEntryForUrl(logger, url);
        assert (entry != null);
        ResourceOracleImpl resourceOracle = new ResourceOracleImpl(Collections.singletonList(entry));
        PathPrefixSet pathPrefixSet = new PathPrefixSet();
        PathPrefix pathPrefix = new PathPrefix(prefix, null, true);
        pathPrefixSet.add(pathPrefix);
        resourceOracle.setPathPrefixes(pathPrefixSet);
        ResourceOracleImpl.refresh(logger, resourceOracle);
        Map<String, Resource> resourceMap = resourceOracle.getResourceMap();
        return resourceMap;
    }

    private void publishAttributeToWebApp(TreeLogger logger, StandardContext webapp, String attrName,
            Object attrValue) {
        if (logger.isLoggable(TreeLogger.TRACE)) {
            logger.log(TreeLogger.TRACE,
                    "Adding attribute  '" + attrName + "' to web app '" + webapp.getName() + "'", null);
        }
        webapp.getServletContext().setAttribute(attrName, attrValue);
    }

    /**
     * Publish the shell's tree logger as an attribute. This attribute is used to
     * find the logger out of the thin air within the shell servlet.
     */
    private void publishShellLoggerAttribute(TreeLogger logger, TreeLogger loggerToPublish,
            StandardContext webapp) {
        final String attr = "com.google.gwt.dev.shell.logger";
        publishAttributeToWebApp(logger, webapp, attr, loggerToPublish);
    }

    /**
     * Publish the shell's work dir as an attribute. This attribute is used to
     * find it out of the thin air within the shell servlet.
     */
    private void publishShellWorkDirsAttribute(TreeLogger logger, WorkDirs workDirs, StandardContext webapp) {
        final String attr = "com.google.gwt.dev.shell.workdirs";
        publishAttributeToWebApp(logger, webapp, attr, workDirs);
    }

    /**
     * Publish to the web app whether it should automatically generate resources.
     */
    private void publishShouldAutoGenerateResourcesAttribute(TreeLogger logger, boolean shouldAutoGenerateResources,
            StandardContext webapp) {
        publishAttributeToWebApp(logger, webapp, "com.google.gwt.dev.shell.shouldAutoGenerateResources",
                shouldAutoGenerateResources);
    }
}