blob: e7d27aca7314403be7a5d1a65312ede370bd546e [file] [log] [blame]
/*
* 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);
}
}