001: /*
002: * Copyright 2006 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.shell.tomcat;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.dev.util.FileOracle;
020: import com.google.gwt.dev.util.FileOracleFactory;
021: import com.google.gwt.util.tools.Utility;
022:
023: import org.apache.catalina.Connector;
024: import org.apache.catalina.ContainerEvent;
025: import org.apache.catalina.ContainerListener;
026: import org.apache.catalina.Engine;
027: import org.apache.catalina.LifecycleException;
028: import org.apache.catalina.Logger;
029: import org.apache.catalina.core.StandardContext;
030: import org.apache.catalina.core.StandardHost;
031: import org.apache.catalina.startup.Embedded;
032: import org.apache.catalina.startup.HostConfig;
033: import org.apache.coyote.tomcat5.CoyoteConnector;
034:
035: import java.io.File;
036: import java.io.FileOutputStream;
037: import java.io.IOException;
038: import java.io.InputStream;
039: import java.lang.reflect.Field;
040: import java.net.InetAddress;
041: import java.net.ServerSocket;
042: import java.net.URL;
043:
044: /**
045: * Wraps an instance of the Tomcat web server used in hosted mode.
046: */
047: public class EmbeddedTomcatServer {
048:
049: static EmbeddedTomcatServer sTomcat;
050:
051: public static int getPort() {
052: return sTomcat.port;
053: }
054:
055: public static synchronized String start(TreeLogger topLogger,
056: int port, File outDir) {
057: if (sTomcat != null) {
058: throw new IllegalStateException(
059: "Embedded Tomcat is already running");
060: }
061:
062: try {
063: new EmbeddedTomcatServer(topLogger, port, outDir);
064: return null;
065: } catch (LifecycleException e) {
066: String msg = e.getMessage();
067: if (msg != null && msg.indexOf("already in use") != -1) {
068: msg = "Port "
069: + port
070: + " is already is use; you probably still have another session active";
071: } else {
072: msg = "Unable to start the embedded Tomcat server; double-check that your configuration is valid";
073: }
074: return msg;
075: }
076: }
077:
078: // Stop the embedded Tomcat server.
079: //
080: public static synchronized void stop() {
081: if (sTomcat != null) {
082: try {
083: sTomcat.catEmbedded.stop();
084: } catch (LifecycleException e) {
085: // There's nothing we can really do about this and the logger is
086: // gone in many scenarios, so we just ignore it.
087: //
088: } finally {
089: sTomcat = null;
090: }
091: }
092: }
093:
094: /**
095: * Returns what local port the Tomcat connector is running on.
096: *
097: * When starting Tomcat with port 0 (i.e. choose an open port), there is just
098: * no way to figure out what port it actually chose. So we're using pure
099: * hackery to steal the port via reflection. The only works because we bundle
100: * Tomcat with GWT and know exactly what version it is.
101: */
102: private static int computeLocalPort(Connector connector) {
103: Throwable caught = null;
104: try {
105: Field phField = CoyoteConnector.class
106: .getDeclaredField("protocolHandler");
107: phField.setAccessible(true);
108: Object protocolHandler = phField.get(connector);
109:
110: Field epField = protocolHandler.getClass()
111: .getDeclaredField("ep");
112: epField.setAccessible(true);
113: Object endPoint = epField.get(protocolHandler);
114:
115: Field ssField = endPoint.getClass().getDeclaredField(
116: "serverSocket");
117: ssField.setAccessible(true);
118: ServerSocket serverSocket = (ServerSocket) ssField
119: .get(endPoint);
120:
121: return serverSocket.getLocalPort();
122: } catch (SecurityException e) {
123: caught = e;
124: } catch (NoSuchFieldException e) {
125: caught = e;
126: } catch (IllegalArgumentException e) {
127: caught = e;
128: } catch (IllegalAccessException e) {
129: caught = e;
130: }
131: throw new RuntimeException(
132: "Failed to retrieve the startup port from Embedded Tomcat",
133: caught);
134: }
135:
136: private Embedded catEmbedded;
137:
138: private Engine catEngine;
139:
140: private StandardHost catHost = null;
141:
142: private int port;
143:
144: private final TreeLogger startupBranchLogger;
145:
146: private EmbeddedTomcatServer(final TreeLogger topLogger,
147: int listeningPort, final File outDir)
148: throws LifecycleException {
149: if (topLogger == null) {
150: throw new NullPointerException("No logger specified");
151: }
152:
153: final TreeLogger logger = topLogger.branch(TreeLogger.INFO,
154: "Starting HTTP on port " + listeningPort, null);
155:
156: startupBranchLogger = logger;
157:
158: // Make myself the one static instance.
159: // NOTE: there is only one small implementation reason that this has
160: // to be a singleton, which is that the commons logger LogFactory insists
161: // on creating your logger class which must have a constructor with
162: // exactly one String argument, and since we want LoggerAdapter to delegate
163: // to the logger instance available through instance host, there is no
164: // way I can think of to delegate without accessing a static field.
165: // An inner class is almost right, except there's no outer instance.
166: //
167: sTomcat = this ;
168:
169: // Assume the working directory is simply the user's current directory.
170: //
171: File topWorkDir = new File(System.getProperty("user.dir"));
172:
173: // Tell Tomcat its base directory so that it won't complain.
174: //
175: String catBase = System.getProperty("catalina.base");
176: if (catBase == null) {
177: catBase = generateDefaultCatalinaBase(logger, topWorkDir);
178: System.setProperty("catalina.base", catBase);
179: }
180:
181: // Some debug messages for ourselves.
182: //
183: logger
184: .log(TreeLogger.DEBUG, "catalina.base = " + catBase,
185: null);
186:
187: // Set up the logger that will be returned by the Commons logging factory.
188: //
189: String adapterClassName = CommonsLoggerAdapter.class.getName();
190: System.setProperty("org.apache.commons.logging.Log",
191: adapterClassName);
192:
193: // And set up an adapter that will work with the Catalina logger family.
194: //
195: Logger catalinaLogger = new CatalinaLoggerAdapter(topLogger);
196:
197: // Create an embedded server.
198: //
199: catEmbedded = new Embedded();
200: catEmbedded.setDebug(0);
201: catEmbedded.setLogger(catalinaLogger);
202:
203: // The embedded engine is called "gwt".
204: //
205: catEngine = catEmbedded.createEngine();
206: catEngine.setName("gwt");
207: catEngine.setDefaultHost("localhost");
208:
209: // It answers localhost requests.
210: //
211: // String appBase = fCatalinaBaseDir.getAbsolutePath();
212: String appBase = catBase + "/webapps";
213: catHost = (StandardHost) catEmbedded.createHost("localhost",
214: appBase);
215:
216: // Hook up a host config to search for and pull in webapps.
217: //
218: HostConfig hostConfig = new HostConfig();
219: catHost.addLifecycleListener(hostConfig);
220:
221: // Hook pre-install events so that we can add attributes to allow loaded
222: // instances to find their development instance host.
223: //
224: catHost.addContainerListener(new ContainerListener() {
225: public void containerEvent(ContainerEvent event) {
226: if (StandardHost.PRE_INSTALL_EVENT.equals(event
227: .getType())) {
228: StandardContext webapp = (StandardContext) event
229: .getData();
230: publishShellLoggerAttribute(logger, topLogger,
231: webapp);
232: publishShellOutDirAttribute(logger, outDir, webapp);
233: }
234: }
235: });
236:
237: // Tell the engine about the host.
238: //
239: catEngine.addChild(catHost);
240: catEngine.setDefaultHost(catHost.getName());
241:
242: // Tell the embedded manager about the engine.
243: //
244: catEmbedded.addEngine(catEngine);
245: InetAddress nullAddr = null;
246: Connector connector = catEmbedded.createConnector(nullAddr,
247: listeningPort, false);
248: catEmbedded.addConnector(connector);
249:
250: // start up!
251: catEmbedded.start();
252: port = computeLocalPort(connector);
253:
254: if (port != listeningPort) {
255: logger.log(TreeLogger.INFO, "HTTP listening on port "
256: + port, null);
257: }
258: }
259:
260: public TreeLogger getLogger() {
261: return startupBranchLogger;
262: }
263:
264: /*
265: * Assumes that the leaf is a file (not a directory).
266: */
267: private void copyFileNoOverwrite(TreeLogger logger,
268: FileOracle fileOracle, String srcResName, File catBase) {
269:
270: File dest = new File(catBase, srcResName);
271: InputStream is = null;
272: FileOutputStream os = null;
273: try {
274: URL srcRes = fileOracle.find(srcResName);
275: if (srcRes == null) {
276: logger.log(TreeLogger.TRACE, "Cannot find: "
277: + srcResName, null);
278: return;
279: }
280:
281: // Only copy if src is newer than desc.
282: //
283: long srcLastModified = srcRes.openConnection()
284: .getLastModified();
285: long dstLastModified = dest.lastModified();
286:
287: if (srcLastModified < dstLastModified) {
288: // Don't copy over it.
289: //
290: logger.log(TreeLogger.TRACE,
291: "Source is older than existing: "
292: + dest.getAbsolutePath(), null);
293: return;
294: } else if (srcLastModified == dstLastModified) {
295: // Exact same time; quietly don't overwrite.
296: //
297: return;
298: }
299:
300: // Make dest directories as required.
301: //
302: File destParent = dest.getParentFile();
303: if (destParent != null) {
304: // No need to check mkdirs result because IOException later anyway.
305: destParent.mkdirs();
306: }
307:
308: // Open in and out streams.
309: //
310: is = srcRes.openStream();
311: os = new FileOutputStream(dest);
312:
313: // Copy the bytes over.
314: //
315: Utility.streamOut(is, os, 1024);
316:
317: // Try to close and change the last modified time.
318: //
319: Utility.close(os);
320: dest.setLastModified(srcLastModified);
321:
322: logger.log(TreeLogger.TRACE, "Wrote: "
323: + dest.getAbsolutePath(), null);
324: } catch (IOException e) {
325: logger.log(TreeLogger.WARN, "Failed to write: "
326: + dest.getAbsolutePath(), e);
327: } finally {
328: Utility.close(is);
329: Utility.close(os);
330: }
331: }
332:
333: /**
334: * Extracts a valid catalina base instance from the classpath. Does not
335: * overwrite any existing files.
336: */
337: private String generateDefaultCatalinaBase(TreeLogger logger,
338: File workDir) {
339: logger = logger
340: .branch(
341: TreeLogger.TRACE,
342: "Property 'catalina.base' not specified; checking for a standard catalina base image instead",
343: null);
344:
345: // Recursively copies out files and directories under
346: // com.google.gwt.dev.etc.tomcat.
347: //
348: FileOracleFactory fof = new FileOracleFactory();
349: final String tomcatEtcDir = "com/google/gwt/dev/etc/tomcat/";
350: fof.addRootPackage(tomcatEtcDir, null);
351: FileOracle fo = fof.create(logger);
352: if (fo.isEmpty()) {
353: logger.log(TreeLogger.WARN, "Could not find "
354: + tomcatEtcDir, null);
355: return null;
356: }
357:
358: File catBase = new File(workDir, "tomcat");
359: String[] allChildren = fo.getAllFiles();
360: for (int i = 0; i < allChildren.length; i++) {
361: String src = allChildren[i];
362: copyFileNoOverwrite(logger, fo, src, catBase);
363: }
364:
365: return catBase.getAbsolutePath();
366: }
367:
368: private void publishAttributeToWebApp(TreeLogger logger,
369: StandardContext webapp, String attrName, Object attrValue) {
370: logger.log(TreeLogger.TRACE, "Adding attribute '" + attrName
371: + "' to web app '" + webapp.getName() + "'", null);
372: webapp.getServletContext().setAttribute(attrName, attrValue);
373: }
374:
375: /**
376: * Publish the shell's tree logger as an attribute. This attribute is used to
377: * find the logger out of the thin air within the shell servlet.
378: */
379: private void publishShellLoggerAttribute(TreeLogger logger,
380: TreeLogger loggerToPublish, StandardContext webapp) {
381: final String attr = "com.google.gwt.dev.shell.logger";
382: publishAttributeToWebApp(logger, webapp, attr, loggerToPublish);
383: }
384:
385: /**
386: * Publish the shell's output dir as an attribute. This attribute is used to
387: * find it out of the thin air within the shell servlet.
388: */
389: private void publishShellOutDirAttribute(TreeLogger logger,
390: File outDirToPublish, StandardContext webapp) {
391: final String attr = "com.google.gwt.dev.shell.outdir";
392: publishAttributeToWebApp(logger, webapp, attr, outDirToPublish);
393: }
394: }
|