| /* |
| * 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); |
| } |
| } |