Implements hot server reloading in GWT via a new Shell button.  Clicking the button restarts embedded Jetty with a new classloader, picking up any class file changes on disk for the restarted server.  This might be useful when changing the type declarations of serializable types during a hosted mode session, or testing how an app handles an incompatible remote service.

Also changes the window title for HostedMode from "Development Shell" to "Hosted Mode".

Review by: jat


git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4597 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/GWTShell.java b/dev/core/src/com/google/gwt/dev/GWTShell.java
index d623526..e0a510b 100644
--- a/dev/core/src/com/google/gwt/dev/GWTShell.java
+++ b/dev/core/src/com/google/gwt/dev/GWTShell.java
@@ -156,6 +156,14 @@
     return new GWTCompilerOptionsImpl(options);
   }
 
+  public WebServerRestart hasWebServer() {
+    return WebServerRestart.NONE;
+  }
+
+  public void restartServer(TreeLogger logger) throws UnableToCompleteException {
+    // Unimplemented.
+  }
+
   public void setCompilerOptions(CompilerOptions options) {
     this.options.copyFrom(options);
   }
@@ -228,6 +236,11 @@
   }
 
   @Override
+  protected String getTitleText() {
+    return "Google Web Toolkit Development Shell";
+  }
+
+  @Override
   protected boolean initModule(String moduleName) {
     /*
      * Not used in legacy mode due to GWTShellServlet playing this role.
diff --git a/dev/core/src/com/google/gwt/dev/HostedMode.java b/dev/core/src/com/google/gwt/dev/HostedMode.java
index 1d47fc3..7ddbc49 100644
--- a/dev/core/src/com/google/gwt/dev/HostedMode.java
+++ b/dev/core/src/com/google/gwt/dev/HostedMode.java
@@ -270,6 +270,15 @@
   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);
@@ -380,6 +389,11 @@
   }
 
   @Override
+  protected String getTitleText() {
+    return "Google Web Toolkit Hosted Mode";
+  }
+
+  @Override
   protected boolean initModule(String moduleName) {
     ModuleDef module = modulesByName.get(moduleName);
     if (module == null) {
@@ -421,8 +435,8 @@
    * 
    * @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
+   * @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)
diff --git a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
index c12aebb..0fac666 100644
--- a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
@@ -178,6 +178,8 @@
     return browserHost;
   }
 
+  protected abstract String getTitleText();
+
   @Override
   protected void initializeLogger() {
     final AbstractTreeLogger logger = mainWnd.getLogger();
@@ -221,8 +223,8 @@
 
     shell.setImages(ShellMainWindow.getIcons());
 
-    mainWnd = new ShellMainWindow(this, shell, options.isNoServer() ? 0
-        : getPort());
+    mainWnd = new ShellMainWindow(this, shell, getTitleText(),
+        options.isNoServer() ? 0 : getPort());
 
     shell.setSize(700, 600);
     if (!isHeadless()) {
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java
index e2bb884..ec178b1 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java
@@ -15,17 +15,29 @@
  */
 package com.google.gwt.dev.shell;
 
+import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 
 /**
  * Interface to the browser window controller.
  */
 public interface BrowserWindowController {
+  /**
+   * Whether to display server control(s).
+   */
+  enum WebServerRestart {
+    DISABLED, ENABLED, NONE
+  }
+
   void closeAllBrowserWindows();
 
   boolean hasBrowserWindowsOpen();
 
+  WebServerRestart hasWebServer();
+
   String normalizeURL(String string);
 
   BrowserWidget openNewBrowserWindow() throws UnableToCompleteException;
-}
\ No newline at end of file
+
+  void restartServer(TreeLogger logger) throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java b/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java
index 0e58953..e9a1e5e 100644
--- a/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java
+++ b/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.shell.BrowserWindowController.WebServerRestart;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
 import com.google.gwt.dev.util.log.TreeLoggerWidget;
@@ -50,6 +51,7 @@
     private ToolItem collapseAll;
     private ToolItem expandAll;
     private ToolItem newWindow;
+    private ToolItem restartServer;
 
     public Toolbar(Composite parent) {
       super(parent);
@@ -69,9 +71,27 @@
           }
         }
       });
-
       newSeparator();
 
+      if (browserWindowController.hasWebServer() != WebServerRestart.NONE) {
+        restartServer = newItem("reload-server.gif", "&Restart Server",
+            "Restart the embedded web server to pick up code changes");
+        restartServer.addSelectionListener(new SelectionAdapter() {
+          @Override
+          public void widgetSelected(SelectionEvent event) {
+            try {
+              browserWindowController.restartServer(getLogger());
+            } catch (UnableToCompleteException e) {
+              getLogger().log(TreeLogger.ERROR, "Unable to restart server", e);
+            }
+          }
+        });
+        newSeparator();
+        if (browserWindowController.hasWebServer() == WebServerRestart.DISABLED) {
+          restartServer.setEnabled(false);
+        }
+      }
+
       collapseAll = newItem("collapse.gif", "&Collapse All",
           "Collapses all log entries");
       collapseAll.addSelectionListener(new SelectionAdapter() {
@@ -181,9 +201,8 @@
   private Toolbar toolbar;
 
   public ShellMainWindow(BrowserWindowController browserWindowController,
-      final Shell parent, int serverPort) {
+      Shell parent, String titleText, int serverPort) {
     super(parent, SWT.NONE);
-
     this.browserWindowController = browserWindowController;
 
     colorWhite = new Color(null, 255, 255, 255);
@@ -193,10 +212,9 @@
 
     setLayout(new FillLayout());
     if (serverPort > 0) {
-      parent.setText("Google Web Toolkit Development Shell / Port "
-          + serverPort);
+      parent.setText(titleText + " / Port " + serverPort);
     } else {
-      parent.setText("Google Web Toolkit Development Shell");
+      parent.setText(titleText);
     }
 
     GridLayout gridLayout = new GridLayout(1, true);
@@ -207,7 +225,6 @@
     setLayout(gridLayout);
 
     // Create the toolbar.
-    //
     {
       toolbar = new Toolbar(this);
       GridData data = new GridData();
@@ -217,7 +234,6 @@
     }
 
     // Create the log pane.
-    //
     {
       logPane = new TreeLoggerWidget(this);
       GridData data = new GridData();
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 1b6023a..d47b05b 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
@@ -133,33 +133,6 @@
     }
   }
 
-  /**
-   * Ensures that only Jetty and other server classes can be loaded into the web
-   * app classloader. This forces the user to put any necessary dependencies
-   * into WEB-INF/lib.
-   */
-  private static final class FilteringParentClassLoader extends ClassLoader {
-    private WebAppClassLoader child = null;
-    private final ClassLoader delegateTo = Thread.currentThread().getContextClassLoader();
-
-    private FilteringParentClassLoader() {
-      super(null);
-    }
-
-    public void setChild(WebAppClassLoader child) {
-      this.child = child;
-    }
-
-    @Override
-    protected Class<?> findClass(String name) throws ClassNotFoundException {
-      if (child != null
-          && (child.isServerPath(name) || child.isSystemPath(name))) {
-        return delegateTo.loadClass(name);
-      }
-      throw new ClassNotFoundException();
-    }
-  }
-
   private static class JettyServletContainer extends ServletContainer {
 
     private final int actualPort;
@@ -216,7 +189,61 @@
     }
   }
 
-  @SuppressWarnings("unchecked")
+  /**
+   * A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload
+   * with a new {@link WebAppClassLoader} to pick up disk changes. The default
+   * Jetty {@code WebAppContext} will create new instances of servlets, but it
+   * will not create a brand new {@link ClassLoader}. By creating a new
+   * {@code ClassLoader} each time, we re-read updated classes from disk.
+   * 
+   * Also provides special class filtering to isolate the web app from the GWT
+   * hosting environment.
+   */
+  private final class WebAppContextWithReload extends WebAppContext {
+    /**
+     * Ensures that only Jetty and other server classes can be loaded into the
+     * {@link WebAppClassLoader}. This forces the user to put any necessary
+     * dependencies into WEB-INF/lib.
+     */
+    private final ClassLoader parentClassLoader = new ClassLoader(null) {
+      private final ClassLoader delegateTo = Thread.currentThread().getContextClassLoader();
+
+      @Override
+      protected Class<?> findClass(String name) throws ClassNotFoundException {
+        if (webAppClassLoader != null
+            && (webAppClassLoader.isServerPath(name) || webAppClassLoader.isSystemPath(name))) {
+          return delegateTo.loadClass(name);
+        }
+        throw new ClassNotFoundException();
+      }
+
+    };
+
+    private WebAppClassLoader webAppClassLoader;
+
+    @SuppressWarnings("unchecked")
+    private WebAppContextWithReload(String webApp, String contextPath) {
+      super(webApp, contextPath);
+      // Prevent file locking on Windows; pick up file changes.
+      getInitParams().put(
+          "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+      webAppClassLoader = new WebAppClassLoader(parentClassLoader, this);
+      setClassLoader(webAppClassLoader);
+      super.doStart();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+      super.doStop();
+      webAppClassLoader = null;
+      setClassLoader(null);
+    }
+  }
+
   public ServletContainer start(TreeLogger logger, int port, File appRootDir)
       throws Exception {
     checkStartParams(logger, port, appRootDir);
@@ -249,15 +276,8 @@
     server.addConnector(connector);
 
     // Create a new web app in the war directory.
-    WebAppContext wac = new WebAppContext(appRootDir.getAbsolutePath(), "/");
-    FilteringParentClassLoader parentClassLoader = new FilteringParentClassLoader();
-    WebAppClassLoader wacl = new WebAppClassLoader(parentClassLoader, wac);
-    parentClassLoader.setChild(wacl);
-    wac.setClassLoader(wacl);
-
-    // Prevent file locking on Windows; pick up file changes.
-    wac.getInitParams().put(
-        "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");
+    WebAppContext wac = new WebAppContextWithReload(
+        appRootDir.getAbsolutePath(), "/");
 
     server.setHandler(wac);
     server.start();
diff --git a/dev/core/src/com/google/gwt/dev/shell/reload-server.gif b/dev/core/src/com/google/gwt/dev/shell/reload-server.gif
new file mode 100644
index 0000000..f98857d
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/reload-server.gif
Binary files differ