Add support for HostedMode with OOPHM.

Patch by: jat, scottb
Review by: scottb, jat


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5036 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java b/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
index 0785fb1..e45e953 100644
--- a/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
+++ b/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
@@ -25,6 +25,22 @@
 public abstract class ServletContainerLauncher {
 
   /**
+   * @return a path to a 24-pixel high image file (relative to the classpath) to
+   *     be used for this servlet container, or null if none.
+   */
+  public String getIconPath() {
+    return null;
+  }
+
+  /**
+   * @return a short human-readable name of this servlet container, or null
+   *     if no name should be displayed.
+   */
+  public String getName() {
+    return "Web Server";
+  }
+
+  /**
    * Start an embedded HTTP servlet container.
    * 
    * @param logger the server logger
diff --git a/dev/core/src/com/google/gwt/dev/HostedMode.java b/dev/core/src/com/google/gwt/dev/HostedMode.java
index 0773528..cea85a4 100644
--- a/dev/core/src/com/google/gwt/dev/HostedMode.java
+++ b/dev/core/src/com/google/gwt/dev/HostedMode.java
@@ -297,6 +297,8 @@
     new Compiler(newOptions).run(logger);
   }
 
+  @Deprecated
+  @Override
   protected void compile(TreeLogger logger, ModuleDef moduleDef)
       throws UnableToCompleteException {
     throw new UnsupportedOperationException();
diff --git a/dev/core/src/com/google/gwt/dev/HostedModeBase.java b/dev/core/src/com/google/gwt/dev/HostedModeBase.java
index 640f813..226a91e 100644
--- a/dev/core/src/com/google/gwt/dev/HostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/HostedModeBase.java
@@ -458,7 +458,6 @@
    * @param logger TreeLogger to use
    * @param typeOracle
    * @param moduleDef
-   * @param genDir
    * @return ShellModuleSpaceHost instance
    */
   protected final ShellModuleSpaceHost doCreateShellModuleSpaceHost(
diff --git a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
index 8a73bb5..ea06563 100644
--- a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
@@ -74,6 +74,16 @@
         widgetShell.setCursor(normalCursor);
       }
     }
+
+    public ModuleSpaceHost createModuleSpaceHost(TreeLogger logger,
+        String moduleName, String userAgent, String remoteEndpoint)
+        throws UnableToCompleteException {
+      throw new UnsupportedOperationException();
+    }
+
+    public void unloadModule(ModuleSpaceHost moduleSpaceHost) {
+      throw new UnsupportedOperationException();
+    }
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
index b917a1b..9253b34 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
@@ -41,10 +41,18 @@
   @Deprecated
   void compile(String[] modules) throws UnableToCompleteException;
 
-  // Factor this out if BrowserWidget becomes decoupled from hosted mode
+  /**
+   * For SWT.
+   */
   ModuleSpaceHost createModuleSpaceHost(TreeLogger logger,
       BrowserWidget widget, String moduleName) throws UnableToCompleteException;
 
+  /**
+   * For OOPHM.
+   */
+  ModuleSpaceHost createModuleSpaceHost(TreeLogger logger, String moduleName,
+      String userAgent, String remoteEndpoint) throws UnableToCompleteException;
+
   TreeLogger getLogger();
 
   /**
@@ -69,5 +77,13 @@
 
   String normalizeURL(String whatTheUserTyped);
 
+  /**
+   * For SWT.
+   */
   BrowserWidget openNewBrowserWindow() throws UnableToCompleteException;
+
+  /**
+   * For OOPHM.
+   */
+  void unloadModule(ModuleSpaceHost moduleSpaceHost);
 }
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
index 5cd8c0b..72a35ba 100644
--- a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -187,10 +187,12 @@
       this.appRootDir = appRootDir;
     }
 
+    @Override
     public int getPort() {
       return actualPort;
     }
 
+    @Override
     public void refresh() throws UnableToCompleteException {
       String msg = "Reloading web app to reflect changes in "
           + appRootDir.getAbsolutePath();
@@ -212,6 +214,7 @@
       branch.log(TreeLogger.INFO, "Reload completed successfully");
     }
 
+    @Override
     public void stop() throws UnableToCompleteException {
       TreeLogger branch = logger.branch(TreeLogger.INFO,
           "Stopping Jetty server");
@@ -416,6 +419,19 @@
     Log.getLog();
   }
 
+  @Override
+  public String getIconPath() {
+    return JettyLauncher.class.getPackage().getName().replace('.', '/')
+        + "/icon24.png";
+  }
+
+  @Override
+  public String getName() {
+    // Use only the icon for the tab.
+    return null;
+  }
+
+  @Override
   public ServletContainer start(TreeLogger logger, int port, File appRootDir)
       throws Exception {
     checkStartParams(logger, port, appRootDir);
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/icon24.png b/dev/core/src/com/google/gwt/dev/shell/jetty/icon24.png
new file mode 100644
index 0000000..f51491e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/icon24.png
Binary files differ
diff --git a/dev/oophm/overlay/com/google/gwt/dev/GWTShell.java b/dev/oophm/overlay/com/google/gwt/dev/GWTShell.java
index eb3df67..bba102a 100644
--- a/dev/oophm/overlay/com/google/gwt/dev/GWTShell.java
+++ b/dev/oophm/overlay/com/google/gwt/dev/GWTShell.java
@@ -17,100 +17,30 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.linker.ArtifactSet;
 import com.google.gwt.core.ext.linker.EmittedArtifact;
-import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.GWTCompiler.GWTCompilerOptionsImpl;
 import com.google.gwt.dev.cfg.ModuleDef;
-import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.shell.ArtifactAcceptor;
-import com.google.gwt.dev.shell.BrowserListener;
 import com.google.gwt.dev.shell.BrowserWidget;
-import com.google.gwt.dev.shell.BrowserWidgetHost;
-import com.google.gwt.dev.shell.ModuleSpaceHost;
-import com.google.gwt.dev.shell.OophmSessionHandler;
-import com.google.gwt.dev.shell.ShellMainWindow;
-import com.google.gwt.dev.shell.ShellModuleSpaceHost;
 import com.google.gwt.dev.shell.WorkDirs;
 import com.google.gwt.dev.shell.tomcat.EmbeddedTomcatServer;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
-import com.google.gwt.dev.util.log.AbstractTreeLogger;
-import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.util.tools.ArgHandlerExtra;
-import com.google.gwt.util.tools.ArgHandlerString;
 
-import java.awt.Cursor;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
 import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
 import java.net.URL;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Map;
 import java.util.Set;
 
 import javax.swing.ImageIcon;
-import javax.swing.JFrame;
-import javax.swing.JTabbedPane;
-import javax.swing.WindowConstants;
 
 /**
  * The main executable class for the hosted mode shell.
  */
 @SuppressWarnings("deprecation")
 @Deprecated
-public class GWTShell extends HostedModeBase {
-
-  /**
-   * Handles the -portHosted command line flag.
-   */
-  protected static class ArgHandlerPortHosted extends ArgHandlerString {
-
-    private final OptionPortHosted options;
-
-    public ArgHandlerPortHosted(OptionPortHosted options) {
-      this.options = options;
-    }
-
-    @Override
-    public String[] getDefaultArgs() {
-      return new String[] {"-portHosted", "9997"};
-    }
-
-    @Override
-    public String getPurpose() {
-      return "Listens on the specified port for hosted mode connections";
-    }
-
-    @Override
-    public String getTag() {
-      return "-portHosted";
-    }
-
-    @Override
-    public String[] getTagArgs() {
-      return new String[] {"port-number | \"auto\""};
-    }
-
-    @Override
-    public boolean setString(String value) {
-      if (value.equals("auto")) {
-        options.setPortHosted(0);
-      } else {
-        try {
-          options.setPortHosted(Integer.parseInt(value));
-        } catch (NumberFormatException e) {
-          System.err.println("A port must be an integer or \"auto\"");
-          return false;
-        }
-      }
-      return true;
-    }
-  }
+public class GWTShell extends OophmHostedModeBase {
 
   /**
    * Handles the list of startup urls that can be passed at the end of the
@@ -144,7 +74,7 @@
   /**
    * The GWTShell argument processor.
    */
-  protected static class ArgProcessor extends HostedModeBase.ArgProcessor {
+  protected static class ArgProcessor extends OophmHostedModeBase.ArgProcessor {
     public ArgProcessor(ShellOptionsImpl options, boolean forceServer,
         boolean noURLs) {
       super(options, forceServer);
@@ -152,7 +82,6 @@
         registerHandler(new ArgHandlerStartupURLsExtra(options));
       }
       registerHandler(new ArgHandlerOutDir(options));
-      registerHandler(new ArgHandlerPortHosted(options));
     }
 
     @Override
@@ -161,20 +90,14 @@
     }
   }
 
-  interface OptionPortHosted {
-    int getPortHosted();
-
-    void setPortHosted(int portHosted);
-  }
-
   /**
    * Concrete class to implement all shell options.
    */
-  static class ShellOptionsImpl extends HostedModeBaseOptionsImpl implements
-      HostedModeBaseOptions, WorkDirs, LegacyCompilerOptions, OptionPortHosted {
+  static class ShellOptionsImpl extends OophmHostedModeBaseOptionsImpl
+      implements HostedModeBaseOptions, WorkDirs, LegacyCompilerOptions,
+      OptionPortHosted {
     private int localWorkers;
     private File outDir;
-    private int portHosted;
 
     public File getCompilerOutputDir(ModuleDef moduleDef) {
       return new File(getOutDir(), moduleDef.getName());
@@ -188,10 +111,6 @@
       return outDir;
     }
 
-    public int getPortHosted() {
-      return portHosted;
-    }
-
     public File getShellPublicGenDir(ModuleDef moduleDef) {
       return new File(getShellBaseWorkDir(moduleDef), "public");
     }
@@ -208,131 +127,11 @@
     public void setOutDir(File outDir) {
       this.outDir = outDir;
     }
-
-    public void setPortHosted(int port) {
-      portHosted = port;
-    }
-  }
-
-  private class BrowserWidgetHostImpl implements BrowserWidgetHost {
-    private TreeLogger logger;
-    private Map<ModuleSpaceHost, ModulePanel> moduleTabs = new IdentityHashMap<ModuleSpaceHost, ModulePanel>();
-
-    public BrowserWidgetHostImpl() {
-    }
-
-    public void compile(ModuleDef moduleDef) throws UnableToCompleteException {
-      GWTShell.this.compile(getLogger(), moduleDef);
-    }
-
-    public void compile(String[] moduleNames) throws UnableToCompleteException {
-      for (int i = 0; i < moduleNames.length; i++) {
-        String moduleName = moduleNames[i];
-        ModuleDef moduleDef = loadModule(moduleName, getLogger());
-        compile(moduleDef);
-      }
-    }
-
-    public ModuleSpaceHost createModuleSpaceHost(BrowserWidget widget,
-        String moduleName) throws UnableToCompleteException {
-      // TODO(jat): implement method createModuleSpaceHost
-      return null;
-    }
-
-    public ModuleSpaceHost createModuleSpaceHost(TreeLogger mainLogger,
-        String moduleName, String userAgent, String remoteSocket)
-        throws UnableToCompleteException {
-      logger = mainLogger;
-      TreeLogger.Type maxLevel = TreeLogger.INFO;
-      if (mainLogger instanceof AbstractTreeLogger) {
-        maxLevel = ((AbstractTreeLogger) mainLogger).getMaxDetail();
-      }
-
-      ModulePanel tab;
-      if (!isHeadless()) {
-        tab = new ModulePanel(maxLevel, moduleName, userAgent, remoteSocket,
-            tabs);
-        logger = tab.getLogger();
-
-        // Switch to a wait cursor.
-        frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
-      } else {
-        tab = null;
-      }
-
-      try {
-        // Try to find an existing loaded version of the module def.
-        ModuleDef moduleDef = loadModule(moduleName, logger);
-        assert (moduleDef != null);
-
-        // Create a sandbox for the module.
-        // TODO(jat): consider multiple instances of the same module open at
-        // once
-        TypeOracle typeOracle = moduleDef.getTypeOracle(logger);
-        ShellModuleSpaceHost host = doCreateShellModuleSpaceHost(logger,
-            typeOracle, moduleDef);
-
-        if (tab != null) {
-          moduleTabs.put(host, tab);
-        }
-        return host;
-      } catch (RuntimeException e) {
-        logger.log(TreeLogger.ERROR, "Exception initializing module", e);
-        throw e;
-      } finally {
-        if (!isHeadless()) {
-          frame.setCursor(Cursor.getDefaultCursor());
-        }
-      }
-    }
-
-    public TreeLogger getLogger() {
-      return logger;
-    }
-
-    public String normalizeURL(String whatTheUserTyped) {
-      return GWTShell.this.normalizeURL(whatTheUserTyped);
-    }
-
-    public BrowserWidget openNewBrowserWindow()
-        throws UnableToCompleteException {
-      // TODO(jat): is this ok?
-      throw new UnableToCompleteException();
-    }
-
-    public void unloadModule(ModuleSpaceHost moduleSpaceHost) {
-      ModulePanel tab = moduleTabs.remove(moduleSpaceHost);
-      if (tab != null) {
-        tab.disconnect();
-      }
-    }
-
-    /**
-     * Load a module.
-     * 
-     * @param moduleName name of the module to load
-     * @param logger TreeLogger to use
-     * @return the loaded module
-     * @throws UnableToCompleteException
-     */
-    private ModuleDef loadModule(String moduleName, TreeLogger logger)
-        throws UnableToCompleteException {
-      // TODO(jat): consider multithreading issues dealing with ModuleDefs
-      boolean assumeFresh = !alreadySeenModules.contains(moduleName);
-      ModuleDef moduleDef = ModuleDefLoader.loadFromClassPath(logger,
-          moduleName, !assumeFresh);
-      alreadySeenModules.add(moduleName);
-      assert (moduleDef != null) : "Required module state is absent";
-      return moduleDef;
-    }
   }
 
   public static final String GWT_SHELL_PATH = ".gwt-tmp" + File.separator
       + "shell";
 
-  private static final String PACKAGE_PATH = GWTShell.class.getPackage().getName().replace(
-      '.', '/').concat("/shell/");
-
   public static String checkHost(String hostUnderConsideration,
       Set<String> hosts) {
     hostUnderConsideration = hostUnderConsideration.toLowerCase();
@@ -394,8 +193,6 @@
     }
   }
 
-  protected BrowserListener listener;
-
   /**
    * Hiding super field because it's actually the same object, just with a
    * stronger type.
@@ -405,92 +202,11 @@
 
   protected File outDir;
 
-  /**
-   * Cheat on the first load's refresh by assuming the module loaded by
-   * {@link com.google.gwt.dev.shell.GWTShellServlet} is still fresh. This
-   * prevents a double-refresh on startup. Subsequent refreshes will trigger a
-   * real refresh.
-   */
-  private Set<String> alreadySeenModules = new HashSet<String>();
-
-  private BrowserWidgetHostImpl browserHost = new BrowserWidgetHostImpl();
-
-  private JFrame frame;
-
-  private volatile boolean mainWindowClosed;
-
-  private ShellMainWindow mainWnd;
-
-  private JTabbedPane tabs;
-
-  private AbstractTreeLogger topLogger;
-
-  private WebServerPanel webServerLog;
-
-  @Override
-  public void closeAllBrowserWindows() {
-  }
-
-  @Override
-  public TreeLogger getTopLogger() {
-    return topLogger;
-  }
-
-  @Override
-  public boolean hasBrowserWindowsOpen() {
-    return false;
-  }
-
   public WebServerRestart hasWebServer() {
     return WebServerRestart.NONE;
   }
 
-  /**
-   * Launch the arguments as Urls in separate windows.
-   */
   @Override
-  public void launchStartupUrls(final TreeLogger logger) {
-    ensureOophmListener();
-    String startupURL = "";
-    try {
-      for (String prenormalized : options.getStartupURLs()) {
-        startupURL = normalizeURL(prenormalized);
-        logger.log(TreeLogger.INFO, "Starting URL: " + startupURL, null);
-        launchURL(startupURL);
-      }
-    } catch (UnableToCompleteException e) {
-      logger.log(TreeLogger.ERROR,
-          "Unable to open new window for startup URL: " + startupURL, null);
-    }
-  }
-
-  public void launchURL(String url) throws UnableToCompleteException {
-    /*
-     * TODO(jat): properly support launching arbitrary browsers; waiting on
-     * Freeland's work with BrowserScanner and the trunk merge to get it.
-     */
-    String separator;
-    if (url.contains("?")) {
-      separator = "&";
-    } else {
-      separator = "?";
-    }
-    url += separator + "gwt.hosted=" + listener.getEndpointIdentifier();
-    TreeLogger branch = getTopLogger().branch(TreeLogger.INFO,
-        "Launching firefox with " + url, null);
-    try {
-      Process browser = Runtime.getRuntime().exec("firefox " + url + "&");
-      int exitCode = browser.waitFor();
-      if (exitCode != 0) {
-        branch.log(TreeLogger.ERROR, "Exit code " + exitCode, null);
-      }
-    } catch (IOException e) {
-      branch.log(TreeLogger.ERROR, "Error starting browser", e);
-    } catch (InterruptedException e) {
-      branch.log(TreeLogger.ERROR, "Error starting browser", e);
-    }
-  }
-
   public BrowserWidget openNewBrowserWindow() throws UnableToCompleteException {
     throw new UnableToCompleteException();
   }
@@ -544,14 +260,6 @@
     };
   }
 
-  /**
-   * Can be override to change the default log level in subclasses. JUnit does
-   * this for example.
-   */
-  protected Type doGetDefaultLogLevel() {
-    return Type.INFO;
-  }
-
   @Override
   protected void doShutDownServer() {
     // Stop the HTTP server.
@@ -560,100 +268,25 @@
   }
 
   @Override
-  protected boolean doStartup() {
-    if (super.doStartup()) {
-      // Accept connections from OOPHM clients
-      ensureOophmListener();
-      return true;
-    }
-    return false;
-  }
-
-  @Override
   protected int doStartUpServer() {
     // TODO(bruce): make tomcat work in terms of the modular launcher
     String whyFailed = EmbeddedTomcatServer.start(isHeadless() ? getTopLogger()
         : webServerLog.getLogger(), getPort(), options);
 
-    // TODO(bruce): test that we can remove this old approach in favor of
-    // a better, logger-based error reporting
     if (whyFailed != null) {
-      System.err.println(whyFailed);
+      getTopLogger().log(TreeLogger.ERROR, "Starting Tomcat: " + whyFailed);
       return -1;
     }
     return EmbeddedTomcatServer.getPort();
   }
 
   @Override
-  protected void initializeLogger() {
-    if (mainWnd != null) {
-      topLogger = mainWnd.getLogger();
-    } else {
-      topLogger = new PrintWriterTreeLogger(new PrintWriter(System.out));
-    }
-    topLogger.setMaxDetail(options.getLogLevel());
+  protected ImageIcon getWebServerIcon() {
+    return loadImageIcon("tomcat24.png");
   }
 
   @Override
-  protected boolean initModule(String moduleName) {
-    /*
-     * Not used in legacy mode due to GWTShellServlet playing this role.
-     * 
-     * TODO: something smarter here and actually make GWTShellServlet less
-     * magic?
-     */
-    return false;
-  }
-
-  @Override
-  protected void loadRequiredNativeLibs() {
-    // no native libraries are needed with OOPHM
-  }
-
-  @Override
-  protected synchronized boolean notDone() {
-    return !mainWindowClosed;
-  }
-
-  @Override
-  protected void openAppWindow() {
-    ImageIcon gwtIcon = loadImageIcon("icon24.png");
-    frame = new JFrame("GWT Hosted Mode");
-    tabs = new JTabbedPane();
-    mainWnd = new ShellMainWindow(this, options.getLogLevel());
-    tabs.addTab("Hosted Mode", gwtIcon, mainWnd, "GWT Hosted-mode");
-    if (!options.isNoServer()) {
-      ImageIcon tomcatIcon = loadImageIcon("tomcat24.png");
-      webServerLog = new WebServerPanel(getPort(), options.getLogLevel());
-      tabs.addTab("Tomcat", tomcatIcon, webServerLog);
-    }
-    frame.getContentPane().add(tabs);
-    frame.setSize(950, 700);
-    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
-    frame.addWindowListener(new WindowAdapter() {
-      @Override
-      public void windowClosed(WindowEvent e) {
-        setMainWindowClosed();
-      }
-    });
-    frame.setIconImage(loadImageIcon("icon16.png").getImage());
-    frame.setVisible(true);
-  }
-
-  @Override
-  protected void processEvents() throws Exception {
-    Thread.sleep(10);
-  }
-
-  private void ensureOophmListener() {
-    if (listener == null) {
-      listener = new BrowserListener(getTopLogger(), options.getPortHosted(),
-          new OophmSessionHandler(browserHost));
-      listener.start();
-    }
-  }
-
-  private synchronized void setMainWindowClosed() {
-    mainWindowClosed = true;
+  protected String getWebServerName() {
+    return "Tomcat";
   }
 }
diff --git a/dev/oophm/overlay/com/google/gwt/dev/HostedMode.java b/dev/oophm/overlay/com/google/gwt/dev/HostedMode.java
new file mode 100644
index 0000000..6429e28
--- /dev/null
+++ b/dev/oophm/overlay/com/google/gwt/dev/HostedMode.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2008 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;
+
+import com.google.gwt.core.ext.ServletContainer;
+import com.google.gwt.core.ext.ServletContainerLauncher;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
+import com.google.gwt.dev.Compiler.CompilerOptionsImpl;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.dev.shell.ArtifactAcceptor;
+import com.google.gwt.dev.shell.jetty.JettyLauncher;
+import com.google.gwt.dev.util.InstalledHelpInfo;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
+import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers;
+import com.google.gwt.dev.util.arg.ArgHandlerModuleName;
+import com.google.gwt.dev.util.arg.ArgHandlerWarDir;
+import com.google.gwt.dev.util.arg.ArgHandlerWorkDirOptional;
+import com.google.gwt.util.tools.ArgHandlerString;
+import com.google.gwt.util.tools.Utility;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.BindException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+
+/**
+ * The main executable class for the hosted mode shell. NOTE: the public API for
+ * this class is to be determined. Consider this class as having <b>no</b>
+ * public API other than {@link #main(String[])}.
+ */
+public class HostedMode extends OophmHostedModeBase {
+
+  /**
+   * Handles the -server command line flag.
+   */
+  protected static class ArgHandlerServer extends ArgHandlerString {
+    private HostedModeOptions options;
+
+    public ArgHandlerServer(HostedModeOptions options) {
+      this.options = options;
+    }
+
+    @Override
+    public String[] getDefaultArgs() {
+      if (options.isNoServer()) {
+        return null;
+      } else {
+        return new String[] {getTag(), JettyLauncher.class.getName()};
+      }
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Specify a different embedded web server to run (must implement ServletContainerLauncher)";
+    }
+
+    @Override
+    public String getTag() {
+      return "-server";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"servletContainerLauncher"};
+    }
+
+    @Override
+    public boolean setString(String sclClassName) {
+      // Supercedes -noserver.
+      options.setNoServer(false);
+      Throwable t;
+      try {
+        Class<?> clazz = Class.forName(sclClassName, true,
+            Thread.currentThread().getContextClassLoader());
+        Class<? extends ServletContainerLauncher> sclClass = clazz.asSubclass(ServletContainerLauncher.class);
+        options.setServletContainerLauncher(sclClass.newInstance());
+        return true;
+      } catch (ClassCastException e) {
+        t = e;
+      } catch (ClassNotFoundException e) {
+        t = e;
+      } catch (InstantiationException e) {
+        t = e;
+      } catch (IllegalAccessException e) {
+        t = e;
+      }
+      System.err.println("Unable to load server class '" + sclClassName + "'");
+      t.printStackTrace();
+      return false;
+    }
+  }
+
+  /**
+   * Handles a startup url that can be passed on the command line.
+   */
+  protected static class ArgHandlerStartupURLs extends ArgHandlerString {
+    private final OptionStartupURLs options;
+
+    public ArgHandlerStartupURLs(OptionStartupURLs options) {
+      this.options = options;
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Automatically launches the specified URL";
+    }
+
+    @Override
+    public String getTag() {
+      return "-startupUrl";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"url"};
+    }
+
+    @Override
+    public boolean setString(String arg) {
+      options.addStartupURL(arg);
+      return true;
+    }
+  }
+
+  static class ArgProcessor extends OophmHostedModeBase.ArgProcessor {
+    public ArgProcessor(HostedModeOptions options) {
+      super(options, false);
+      registerHandler(new ArgHandlerServer(options));
+      registerHandler(new ArgHandlerStartupURLs(options));
+      registerHandler(new ArgHandlerWarDir(options));
+      registerHandler(new ArgHandlerExtraDir(options));
+      registerHandler(new ArgHandlerWorkDirOptional(options));
+      registerHandler(new ArgHandlerLocalWorkers(options));
+      registerHandler(new ArgHandlerModuleName(options) {
+        @Override
+        public String getPurpose() {
+          return super.getPurpose() + " to host";
+        }
+      });
+    }
+
+    @Override
+    protected String getName() {
+      return HostedMode.class.getName();
+    }
+  }
+
+  interface HostedModeOptions extends OophmHostedModeBaseOptions,
+      CompilerOptions {
+    ServletContainerLauncher getServletContainerLauncher();
+
+    void setServletContainerLauncher(ServletContainerLauncher scl);
+  }
+
+  /**
+   * Concrete class to implement all hosted mode options.
+   */
+  static class HostedModeOptionsImpl extends OophmHostedModeBaseOptionsImpl
+      implements HostedModeOptions {
+    private File extraDir;
+    private int localWorkers;
+    private File outDir;
+    private ServletContainerLauncher scl;
+    private File warDir;
+
+    public File getExtraDir() {
+      return extraDir;
+    }
+
+    public int getLocalWorkers() {
+      return localWorkers;
+    }
+
+    @Deprecated
+    public File getOutDir() {
+      return outDir;
+    }
+
+    public ServletContainerLauncher getServletContainerLauncher() {
+      return scl;
+    }
+
+    public File getShellBaseWorkDir(ModuleDef moduleDef) {
+      return new File(new File(getWorkDir(), moduleDef.getName()), "shell");
+    }
+
+    public File getShellPublicGenDir(ModuleDef moduleDef) {
+      return new File(getShellBaseWorkDir(moduleDef), "public");
+    }
+
+    public File getWarDir() {
+      return warDir;
+    }
+
+    public void setExtraDir(File extraDir) {
+      this.extraDir = extraDir;
+    }
+
+    public void setLocalWorkers(int localWorkers) {
+      this.localWorkers = localWorkers;
+    }
+
+    @Deprecated
+    public void setOutDir(File outDir) {
+      this.outDir = outDir;
+    }
+
+    public void setServletContainerLauncher(ServletContainerLauncher scl) {
+      this.scl = scl;
+    }
+
+    public void setWarDir(File warDir) {
+      this.warDir = warDir;
+    }
+  }
+
+  public static void main(String[] args) {
+    /*
+     * NOTE: main always exits with a call to System.exit to terminate any
+     * non-daemon threads that were started in Generators. Typically, this is to
+     * shutdown AWT related threads, since the contract for their termination is
+     * still implementation-dependent.
+     */
+    HostedMode hostedMode = new HostedMode();
+    if (new ArgProcessor(hostedMode.options).processArgs(args)) {
+      hostedMode.run();
+      // Exit w/ success code.
+      System.exit(0);
+    }
+    // Exit w/ non-success code.
+    System.exit(-1);
+  }
+
+  /**
+   * Hiding super field because it's actually the same object, just with a
+   * stronger type.
+   */
+  @SuppressWarnings("hiding")
+  protected final HostedModeOptionsImpl options = (HostedModeOptionsImpl) super.options;
+
+  /**
+   * Maps each active linker stack by module.
+   */
+  private final Map<String, StandardLinkerContext> linkerStacks = new HashMap<String, StandardLinkerContext>();
+
+  /**
+   * The set of specified modules by name; the keys represent the renamed name
+   * of each module rather than the canonical name.
+   */
+  private Map<String, ModuleDef> modulesByName = new HashMap<String, ModuleDef>();
+
+  /**
+   * The server that was started.
+   */
+  private ServletContainer server;
+
+  /**
+   * Tracks whether we created a temp workdir that we need to destroy.
+   */
+  private boolean tempWorkDir = false;
+
+  /**
+   * Default constructor for testing; no public API yet.
+   */
+  HostedMode() {
+  }
+
+  public WebServerRestart hasWebServer() {
+    return options.isNoServer() ? WebServerRestart.DISABLED
+        : WebServerRestart.ENABLED;
+  }
+
+  public void restartServer(TreeLogger logger) throws UnableToCompleteException {
+    server.refresh();
+  }
+
+  @Override
+  protected void compile(TreeLogger logger) throws UnableToCompleteException {
+    CompilerOptions newOptions = new CompilerOptionsImpl(options);
+    new Compiler(newOptions).run(logger);
+  }
+
+  @Deprecated
+  @Override
+  protected void compile(TreeLogger logger, ModuleDef moduleDef)
+      throws UnableToCompleteException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  protected HostedModeBaseOptions createOptions() {
+    return new HostedModeOptionsImpl();
+  }
+
+  @Override
+  protected ArtifactAcceptor doCreateArtifactAcceptor(final ModuleDef module) {
+    return new ArtifactAcceptor() {
+      public void accept(TreeLogger logger, ArtifactSet newlyGeneratedArtifacts)
+          throws UnableToCompleteException {
+        relink(logger, module, newlyGeneratedArtifacts);
+      }
+    };
+  }
+
+  @Override
+  protected void doShutDownServer() {
+    if (server != null) {
+      try {
+        server.stop();
+      } catch (UnableToCompleteException e) {
+        // Already logged.
+      }
+      server = null;
+    }
+
+    if (tempWorkDir) {
+      Util.recursiveDelete(options.getWorkDir(), false);
+    }
+  }
+
+  @Override
+  protected boolean doStartup() {
+    if (!super.doStartup()) {
+      return false;
+    }
+    tempWorkDir = options.getWorkDir() == null;
+    if (tempWorkDir) {
+      try {
+        options.setWorkDir(Utility.makeTemporaryDirectory(null, "gwtc"));
+      } catch (IOException e) {
+        System.err.println("Unable to create hosted mode work directory");
+        e.printStackTrace();
+        return false;
+      }
+    }
+
+    ServletValidator servletValidator = null;
+    File webXml = new File(options.getWarDir(), "WEB-INF/web.xml");
+    if (webXml.exists()) {
+      servletValidator = ServletValidator.create(getTopLogger(), webXml);
+    }
+
+    for (String moduleName : options.getModuleNames()) {
+      TreeLogger loadLogger = getTopLogger().branch(TreeLogger.DEBUG,
+          "Bootstrap link for command-line module '" + moduleName + "'");
+      try {
+        ModuleDef module = loadModule(loadLogger, moduleName, false);
+        validateServletTags(loadLogger, servletValidator, module, webXml);
+        link(loadLogger, module);
+      } catch (UnableToCompleteException e) {
+        // Already logged.
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  protected int doStartUpServer() {
+    try {
+      TreeLogger serverLogger = webServerLog.getLogger();
+      serverLogger.log(TreeLogger.INFO, "Starting HTTP on port " + getPort(),
+          null);
+      server = options.getServletContainerLauncher().start(serverLogger,
+          getPort(), options.getWarDir());
+      assert (server != null);
+      return server.getPort();
+    } catch (BindException e) {
+      System.err.println("Port "
+          + getPort()
+          + " is already is use; you probably still have another session active");
+    } catch (Exception e) {
+      System.err.println("Unable to start embedded HTTP server");
+      e.printStackTrace();
+    }
+    return -1;
+  }
+
+  @Override
+  protected String getHost() {
+    if (server != null) {
+      return server.getHost();
+    }
+    return super.getHost();
+  }
+
+  @Override
+  protected ImageIcon getWebServerIcon() {
+    return loadImageIcon(options.getServletContainerLauncher().getIconPath(),
+        false);
+  }
+
+  @Override
+  protected String getWebServerName() {
+    return options.getServletContainerLauncher().getName();
+  }
+
+  @Override
+  protected boolean initModule(String moduleName) {
+    ModuleDef module = modulesByName.get(moduleName);
+    if (module == null) {
+      getTopLogger().log(
+          TreeLogger.WARN,
+          "Unknown module requested '"
+              + moduleName
+              + "'; all active GWT modules must be specified in the command line arguments");
+      return false;
+    }
+    try {
+      TreeLogger logger = getTopLogger().branch(TreeLogger.DEBUG,
+          "Initializing module '" + module.getName() + "' for hosted mode");
+      boolean shouldRefreshPage = false;
+      if (module.isGwtXmlFileStale()) {
+        shouldRefreshPage = true;
+        module = loadModule(logger, module.getCanonicalName(), false);
+      }
+      link(logger, module);
+      return shouldRefreshPage;
+    } catch (UnableToCompleteException e) {
+      // Already logged.
+      return false;
+    }
+  }
+
+  /*
+   * Overridden to keep our map up to date.
+   */
+  @Override
+  protected ModuleDef loadModule(TreeLogger logger, String moduleName,
+      boolean refresh) throws UnableToCompleteException {
+    ModuleDef module = super.loadModule(logger, moduleName, refresh);
+    modulesByName.put(module.getName(), module);
+    return module;
+  }
+
+  /**
+   * Perform an initial hosted mode link, without overwriting newer or
+   * unmodified files in the output folder.
+   * 
+   * @param logger the logger to use
+   * @param module the module to link
+   * @param includePublicFiles if <code>true</code>, include public files in
+   *          the link, otherwise do not include them
+   * @throws UnableToCompleteException
+   */
+  private void link(TreeLogger logger, ModuleDef module)
+      throws UnableToCompleteException {
+    TreeLogger linkLogger = logger.branch(TreeLogger.DEBUG, "Linking module '"
+        + module.getName() + "'");
+
+    // TODO: move the module-specific computations to a helper function.
+    File moduleOutDir = new File(options.getWarDir(), module.getName());
+    File moduleExtraDir = (options.getExtraDir() == null) ? null : new File(
+        options.getExtraDir(), module.getName());
+
+    // Create a new active linker stack for the fresh link.
+    StandardLinkerContext linkerStack = new StandardLinkerContext(linkLogger,
+        module, options);
+    linkerStacks.put(module.getName(), linkerStack);
+
+    ArtifactSet artifacts = linkerStack.invokeLink(linkLogger);
+    linkerStack.produceOutputDirectory(linkLogger, artifacts, moduleOutDir,
+        moduleExtraDir);
+  }
+
+  /**
+   * Perform hosted mode relink when new artifacts are generated, without
+   * overwriting newer or unmodified files in the output folder.
+   * 
+   * @param logger the logger to use
+   * @param module the module to link
+   * @param newlyGeneratedArtifacts the set of new artifacts
+   * @throws UnableToCompleteException
+   */
+  private void relink(TreeLogger logger, ModuleDef module,
+      ArtifactSet newlyGeneratedArtifacts) throws UnableToCompleteException {
+    TreeLogger linkLogger = logger.branch(TreeLogger.DEBUG,
+        "Relinking module '" + module.getName() + "'");
+
+    // TODO: move the module-specific computations to a helper function.
+    File moduleOutDir = new File(options.getWarDir(), module.getName());
+    File moduleExtraDir = (options.getExtraDir() == null) ? null : new File(
+        options.getExtraDir(), module.getName());
+
+    // Find the existing linker stack.
+    StandardLinkerContext linkerStack = linkerStacks.get(module.getName());
+    assert linkerStack != null;
+
+    ArtifactSet artifacts = linkerStack.invokeRelink(linkLogger,
+        newlyGeneratedArtifacts);
+    linkerStack.produceOutputDirectory(linkLogger, artifacts, moduleOutDir,
+        moduleExtraDir);
+  }
+
+  private void validateServletTags(TreeLogger logger,
+      ServletValidator servletValidator, ModuleDef module, File webXml) {
+    TreeLogger servletLogger = logger.branch(TreeLogger.DEBUG,
+        "Validating <servlet> tags for module '" + module.getName() + "'",
+        null, new InstalledHelpInfo("servletMappings.html"));
+    String[] servletPaths = module.getServletPaths();
+    if (servletValidator == null && servletPaths.length > 0) {
+      servletLogger.log(
+          TreeLogger.WARN,
+          "Module declares "
+              + servletPaths.length
+              + " <servlet> declaration(s), but a valid 'web.xml' was not found at '"
+              + webXml.getAbsolutePath() + "'");
+    } else {
+      for (String servletPath : servletPaths) {
+        String servletClass = module.findServletForPath(servletPath);
+        assert (servletClass != null);
+        // Prefix module name to convert module mapping to global mapping.
+        servletPath = "/" + module.getName() + servletPath;
+        servletValidator.validate(servletLogger, servletClass, servletPath);
+      }
+    }
+  }
+}
diff --git a/dev/oophm/overlay/com/google/gwt/dev/shell/BrowserWidgetHost.java b/dev/oophm/overlay/com/google/gwt/dev/shell/BrowserWidgetHost.java
index 703f90d..e69de29 100644
--- a/dev/oophm/overlay/com/google/gwt/dev/shell/BrowserWidgetHost.java
+++ b/dev/oophm/overlay/com/google/gwt/dev/shell/BrowserWidgetHost.java
@@ -1,39 +0,0 @@
-/*
- * 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;
-
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.cfg.ModuleDef;
-
-/**
- * Interface that unifies access to the <code>BrowserWidget</code>,
- * <code>ModuleSpaceHost</code>, and the compiler.
- */
-public interface BrowserWidgetHost {
-  void compile(String[] modules) throws UnableToCompleteException;
-
-  void compile(ModuleDef module) throws UnableToCompleteException;
-
-  ModuleSpaceHost createModuleSpaceHost(TreeLogger logger, String moduleName, String userAgent,
-      String remoteEndpoint) throws UnableToCompleteException;
-
-  TreeLogger getLogger();
-
-  String normalizeURL(String whatTheUserTyped);
-
-  void unloadModule(ModuleSpaceHost moduleSpaceHost);
-}
diff --git a/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java b/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java
index 57ab665..97075fd 100644
--- a/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java
+++ b/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java
@@ -16,7 +16,6 @@
 package com.google.gwt.dev.shell;
 
 import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.dev.GWTShell;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
 import com.google.gwt.dev.util.log.SwingLoggerPanel;
 
@@ -34,7 +33,7 @@
 
   private SwingLoggerPanel logWindow;
 
-  public ShellMainWindow(GWTShell shell, TreeLogger.Type maxLevel) {
+  public ShellMainWindow(TreeLogger.Type maxLevel) {
     super(new BorderLayout());
     JPanel panel = new JPanel(new GridLayout(2, 1));
     JPanel optionPanel = new JPanel();
diff --git a/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java b/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
new file mode 100644
index 0000000..8c1d21e
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2008 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;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.WebServerPanel.RestartAction;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.dev.shell.BrowserListener;
+import com.google.gwt.dev.shell.BrowserWidget;
+import com.google.gwt.dev.shell.BrowserWidgetHost;
+import com.google.gwt.dev.shell.ModuleSpaceHost;
+import com.google.gwt.dev.shell.OophmSessionHandler;
+import com.google.gwt.dev.shell.ShellMainWindow;
+import com.google.gwt.dev.shell.ShellModuleSpaceHost;
+import com.google.gwt.dev.util.log.AbstractTreeLogger;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.util.tools.ArgHandlerString;
+
+import java.awt.Cursor;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+import javax.swing.JFrame;
+import javax.swing.JTabbedPane;
+import javax.swing.WindowConstants;
+
+/**
+ * The main executable class for hosted mode shells based on SWT.
+ */
+abstract class OophmHostedModeBase extends HostedModeBase {
+
+  /**
+   * Handles the -portHosted command line flag.
+   */
+  private static class ArgHandlerPortHosted extends ArgHandlerString {
+
+    private final OptionPortHosted options;
+
+    public ArgHandlerPortHosted(OptionPortHosted options) {
+      this.options = options;
+    }
+
+    @Override
+    public String[] getDefaultArgs() {
+      return new String[] {"-portHosted", "9997"};
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Listens on the specified port for hosted mode connections";
+    }
+
+    @Override
+    public String getTag() {
+      return "-portHosted";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"port-number | \"auto\""};
+    }
+
+    @Override
+    public boolean setString(String value) {
+      if (value.equals("auto")) {
+        options.setPortHosted(0);
+      } else {
+        try {
+          options.setPortHosted(Integer.parseInt(value));
+        } catch (NumberFormatException e) {
+          System.err.println("A port must be an integer or \"auto\"");
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+
+  abstract static class ArgProcessor extends HostedModeBase.ArgProcessor {
+    public ArgProcessor(OophmHostedModeBaseOptions options, boolean forceServer) {
+      super(options, forceServer);
+      registerHandler(new ArgHandlerPortHosted(options));
+    }
+  }
+
+  interface OophmHostedModeBaseOptions extends HostedModeBaseOptions,
+      OptionPortHosted {
+  }
+
+  /**
+   * Concrete class to implement all shell options.
+   */
+  static class OophmHostedModeBaseOptionsImpl extends HostedModeBaseOptionsImpl
+      implements OophmHostedModeBaseOptions {
+    private int portHosted;
+
+    public int getPortHosted() {
+      return portHosted;
+    }
+
+    public void setPortHosted(int port) {
+      portHosted = port;
+    }
+  }
+
+  interface OptionPortHosted {
+    int getPortHosted();
+
+    void setPortHosted(int portHosted);
+  }
+
+  private class OophmBrowserWidgetHostImpl extends BrowserWidgetHostImpl {
+    private final Map<ModuleSpaceHost, ModulePanel> moduleTabs = new IdentityHashMap<ModuleSpaceHost, ModulePanel>();
+
+    public ModuleSpaceHost createModuleSpaceHost(TreeLogger logger,
+        BrowserWidget widget, String moduleName)
+        throws UnableToCompleteException {
+      throw new UnsupportedOperationException();
+    }
+
+    public ModuleSpaceHost createModuleSpaceHost(TreeLogger mainLogger,
+        String moduleName, String userAgent, String remoteSocket)
+        throws UnableToCompleteException {
+      TreeLogger logger = mainLogger;
+      TreeLogger.Type maxLevel = TreeLogger.INFO;
+      if (mainLogger instanceof AbstractTreeLogger) {
+        maxLevel = ((AbstractTreeLogger) mainLogger).getMaxDetail();
+      }
+
+      ModulePanel tab;
+      if (!isHeadless()) {
+        tab = new ModulePanel(maxLevel, moduleName, userAgent, remoteSocket,
+            tabs);
+        logger = tab.getLogger();
+
+        // Switch to a wait cursor.
+        frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+      } else {
+        tab = null;
+      }
+
+      try {
+        // Try to find an existing loaded version of the module def.
+        ModuleDef moduleDef = loadModule(logger, moduleName, true);
+        assert (moduleDef != null);
+
+        // Create a sandbox for the module.
+        // TODO(jat): consider multiple instances of the same module open at
+        // once
+        TypeOracle typeOracle = moduleDef.getTypeOracle(logger);
+        ShellModuleSpaceHost host = doCreateShellModuleSpaceHost(logger,
+            typeOracle, moduleDef);
+
+        if (tab != null) {
+          moduleTabs.put(host, tab);
+        }
+        return host;
+      } catch (RuntimeException e) {
+        logger.log(TreeLogger.ERROR, "Exception initializing module", e);
+        throw e;
+      } finally {
+        if (!isHeadless()) {
+          frame.setCursor(Cursor.getDefaultCursor());
+        }
+      }
+    }
+
+    public void unloadModule(ModuleSpaceHost moduleSpaceHost) {
+      ModulePanel tab = moduleTabs.remove(moduleSpaceHost);
+      if (tab != null) {
+        tab.disconnect();
+      }
+    }
+  }
+
+  protected static final String PACKAGE_PATH = OophmHostedModeBase.class.getPackage().getName().replace(
+      '.', '/').concat("/shell/");
+
+  /**
+   * Loads an image from the classpath in this package.
+   */
+  static ImageIcon loadImageIcon(String name) {
+    return loadImageIcon(name, true);
+  }
+
+  /**
+   * Loads an image from the classpath, optionally prepending this package.
+   * 
+   * @param name name of an image file.
+   * @param prependPackage true if {@link #PACKAGE_PATH} should be prepended to
+   *          this name.
+   */
+  static ImageIcon loadImageIcon(String name, boolean prependPackage) {
+    ClassLoader cl = OophmHostedModeBase.class.getClassLoader();
+    if (prependPackage) {
+      name = PACKAGE_PATH + name;
+    }
+    URL url = cl.getResource(name);
+    if (url != null) {
+      ImageIcon image = new ImageIcon(url);
+      return image;
+    } else {
+      // Bad image.
+      return new ImageIcon();
+    }
+  }
+
+  protected BrowserListener listener;
+
+  /**
+   * Hiding super field because it's actually the same object, just with a
+   * stronger type.
+   */
+  @SuppressWarnings("hiding")
+  protected final OophmHostedModeBaseOptionsImpl options = (OophmHostedModeBaseOptionsImpl) super.options;
+
+  // TODO(jat): clean up access to this field
+  protected WebServerPanel webServerLog;
+
+  private BrowserWidgetHostImpl browserHost = new OophmBrowserWidgetHostImpl();
+
+  private JFrame frame;
+
+  private volatile boolean mainWindowClosed;
+
+  private ShellMainWindow mainWnd;
+
+  private JTabbedPane tabs;
+
+  private AbstractTreeLogger topLogger;
+
+  public OophmHostedModeBase() {
+    super();
+  }
+
+  @Override
+  public void closeAllBrowserWindows() {
+  }
+
+  @Override
+  public TreeLogger getTopLogger() {
+    return topLogger;
+  }
+
+  @Override
+  public boolean hasBrowserWindowsOpen() {
+    return false;
+  }
+
+  /**
+   * Launch the arguments as Urls in separate windows.
+   */
+  @Override
+  public void launchStartupUrls(final TreeLogger logger) {
+    ensureOophmListener();
+    String startupURL = "";
+    try {
+      for (String prenormalized : options.getStartupURLs()) {
+        startupURL = normalizeURL(prenormalized);
+        logger.log(TreeLogger.INFO, "Starting URL: " + startupURL, null);
+        launchURL(startupURL);
+      }
+    } catch (UnableToCompleteException e) {
+      logger.log(TreeLogger.ERROR,
+          "Unable to open new window for startup URL: " + startupURL, null);
+    }
+  }
+
+  public void launchURL(String url) throws UnableToCompleteException {
+    /*
+     * TODO(jat): properly support launching arbitrary browsers; waiting on
+     * Freeland's work with BrowserScanner and the trunk merge to get it.
+     */
+    String separator;
+    if (url.contains("?")) {
+      separator = "&";
+    } else {
+      separator = "?";
+    }
+    url += separator + "gwt.hosted=" + listener.getEndpointIdentifier();
+    TreeLogger branch = getTopLogger().branch(TreeLogger.INFO,
+        "Launching firefox with " + url, null);
+    try {
+      Process browser = Runtime.getRuntime().exec("firefox " + url + "&");
+      int exitCode = browser.waitFor();
+      if (exitCode != 0) {
+        branch.log(TreeLogger.ERROR, "Exit code " + exitCode, null);
+      }
+    } catch (IOException e) {
+      branch.log(TreeLogger.ERROR, "Error starting browser", e);
+    } catch (InterruptedException e) {
+      branch.log(TreeLogger.ERROR, "Error starting browser", e);
+    }
+  }
+
+  public BrowserWidget openNewBrowserWindow() throws UnableToCompleteException {
+    throw new UnableToCompleteException();
+  }
+
+  /**
+   * @throws UnableToCompleteException
+   */
+  @Override
+  protected void compile(TreeLogger logger) throws UnableToCompleteException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  protected boolean doStartup() {
+    if (super.doStartup()) {
+      // Accept connections from OOPHM clients
+      ensureOophmListener();
+      return true;
+    }
+    return false;
+  }
+
+  protected final BrowserWidgetHost getBrowserHost() {
+    return browserHost;
+  }
+
+  /**
+   * @return the icon to use for the web server tab
+   */
+  protected ImageIcon getWebServerIcon() {
+    return null;
+  }
+
+  /**
+   * @return the name of the web server tab
+   */
+  protected String getWebServerName() {
+    return "Server";
+  }
+
+  @Override
+  protected void initializeLogger() {
+    if (mainWnd != null) {
+      topLogger = mainWnd.getLogger();
+    } else {
+      topLogger = new PrintWriterTreeLogger(new PrintWriter(System.out));
+    }
+    topLogger.setMaxDetail(options.getLogLevel());
+  }
+
+  @Override
+  protected boolean initModule(String moduleName) {
+    /*
+     * Not used in legacy mode due to GWTShellServlet playing this role.
+     * 
+     * TODO: something smarter here and actually make GWTShellServlet less
+     * magic?
+     */
+    return false;
+  }
+
+  @Override
+  protected void loadRequiredNativeLibs() {
+    // no native libraries are needed with OOPHM
+  }
+
+  @Override
+  protected synchronized boolean notDone() {
+    return !mainWindowClosed;
+  }
+
+  @Override
+  protected void openAppWindow() {
+    ImageIcon gwtIcon = loadImageIcon("icon24.png");
+    frame = new JFrame("GWT Hosted Mode");
+    tabs = new JTabbedPane();
+    mainWnd = new ShellMainWindow(options.getLogLevel());
+    tabs.addTab("Hosted Mode", gwtIcon, mainWnd, "GWT Hosted-mode");
+    if (!options.isNoServer()) {
+      webServerLog = new WebServerPanel(getPort(), options.getLogLevel(),
+          new RestartAction() {
+            public void restartServer(TreeLogger logger) {
+              try {
+                OophmHostedModeBase.this.restartServer(logger);
+              } catch (UnableToCompleteException e) {
+                // Already logged why it failed
+              }
+            }
+          });
+      tabs.addTab(getWebServerName(), getWebServerIcon(), webServerLog);
+    }
+    frame.getContentPane().add(tabs);
+    frame.setSize(950, 700);
+    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+    frame.addWindowListener(new WindowAdapter() {
+      @Override
+      public void windowClosed(WindowEvent e) {
+        setMainWindowClosed();
+      }
+    });
+    frame.setIconImage(loadImageIcon("icon16.png").getImage());
+    frame.setVisible(true);
+  }
+
+  @Override
+  protected void processEvents() throws Exception {
+    Thread.sleep(10);
+  }
+
+  protected synchronized void setMainWindowClosed() {
+    mainWindowClosed = true;
+  }
+
+  private void ensureOophmListener() {
+    if (listener == null) {
+      listener = new BrowserListener(getTopLogger(), options.getPortHosted(),
+          new OophmSessionHandler(browserHost));
+      listener.start();
+    }
+  }
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java b/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java
index 0b3ec1e..3536603 100644
--- a/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java
+++ b/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java
@@ -19,17 +19,44 @@
 import com.google.gwt.dev.util.log.SwingLoggerPanel;
 
 import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 
+import javax.swing.JButton;
 import javax.swing.JPanel;
 
 /**
  */
 public class WebServerPanel extends JPanel {
+  
+  /**
+   * Callback interface for when the server should be restarted.
+   */
+  public interface RestartAction {
+    void restartServer(TreeLogger logger);
+  }
+  
   private SwingLoggerPanel logWindow;
 
   public WebServerPanel(int serverPort, TreeLogger.Type maxLevel) {
+    this(serverPort, maxLevel, null);
+  }
+
+  public WebServerPanel(int serverPort, TreeLogger.Type maxLevel,
+      final RestartAction restartServerAction) {
     super(new BorderLayout());
     logWindow = new SwingLoggerPanel(maxLevel);
+    if (restartServerAction != null) {
+      JPanel panel = new JPanel();
+      JButton restartButton = new JButton("Restart Server");
+      restartButton.addActionListener(new ActionListener() {
+        public void actionPerformed(ActionEvent e) {
+          restartServerAction.restartServer(getLogger());
+        }
+      });
+      panel.add(restartButton);
+      add(panel, BorderLayout.NORTH);
+    }
     add(logWindow);
   }
 
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserListener.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserListener.java
index b06c46a..66d9421 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserListener.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserListener.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.shell;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.shell.BrowserChannel.SessionHandler;
 
 import java.io.IOException;
@@ -88,7 +89,11 @@
     }
   }
 
-  public String getEndpointIdentifier() {
+  public String getEndpointIdentifier() throws UnableToCompleteException {
+    if (listenSocket == null) {
+      // If we failed to initialize our socket, just bail here.
+      throw new UnableToCompleteException();
+    }
     try {
       return InetAddress.getLocalHost().getHostAddress() + ":"
           + listenSocket.getLocalPort();
diff --git a/user/src/com/google/gwt/user/tools/WebAppCreator.java b/user/src/com/google/gwt/user/tools/WebAppCreator.java
index 280372e..8896d97 100644
--- a/user/src/com/google/gwt/user/tools/WebAppCreator.java
+++ b/user/src/com/google/gwt/user/tools/WebAppCreator.java
@@ -215,6 +215,7 @@
     String gwtUserPath = installPath + '/' + "gwt-user.jar";
     String gwtDevPath = installPath + '/' + Utility.getDevJarName();
     String gwtServletPath = installPath + '/' + "gwt-servlet.jar";
+    String gwtOophmPath = installPath + '/' + "gwt-dev-oophm.jar";
 
     // Public builds generate a DTD reference.
     String gwtModuleDtd = "";
@@ -254,6 +255,7 @@
     replacements.put("@gwtSdk", installPath);
     replacements.put("@gwtUserPath", gwtUserPath);
     replacements.put("@gwtDevPath", gwtDevPath);
+    replacements.put("@gwtOophmPath", gwtOophmPath);
     replacements.put("@gwtVersion", About.GWT_VERSION_NUM);
     replacements.put("@gwtModuleDtd", gwtModuleDtd);
     replacements.put("@shellClass", HostedMode.class.getName());
diff --git a/user/src/com/google/gwt/user/tools/project.ant.xmlsrc b/user/src/com/google/gwt/user/tools/project.ant.xmlsrc
index 8b3ef42..a98d4ea 100644
--- a/user/src/com/google/gwt/user/tools/project.ant.xmlsrc
+++ b/user/src/com/google/gwt/user/tools/project.ant.xmlsrc
@@ -55,6 +55,21 @@
       <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
       <arg value="@moduleName"/>
     </java>
+  </target>
+
+  <target name="oophm" depends="javac" description="Run OOPHNM hosted mode">
+    <java failonerror="true" fork="true" classname="@shellClass">
+      <classpath>
+        <pathelement location="${gwt.sdk}/gwt-dev-oophm.jar"/>
+        <pathelement location="src"/>
+        <path refid="project.class.path"/>
+      </classpath>
+      <jvmarg value="-Xmx256M"/>@antVmargs
+      <arg value="-startupUrl"/>
+      <arg value="@startupUrl"/>
+      <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
+      <arg value="@moduleName"/>
+    </java>
   </target>@antEclipseRule
 
   <target name="build" depends="gwtc" description="Build this project" />