All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.dev.shell.tomcat.EmbeddedTomcatServer Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * 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.
    //
    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) {
      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.
        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);

      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 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 entry : resourceMap.entrySet()) {
        copyFileNoOverwrite(logger, entry.getKey(), entry.getValue(), catBase);
      }
    }

    return catBase.getAbsolutePath();
  }

  /**
   * Hacky, but fast.
   */
  private Map 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 resourceMap = resourceOracle.getResourceMap();
    return resourceMap;
  }

  private void publishAttributeToWebApp(TreeLogger logger,
      StandardContext webapp, String attrName, Object attrValue) {
    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);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy