Merging from releases/1.6@3878:3944

svn merge -r3878:3944 https://google-web-toolkit.googlecode.com/svn/releases/1.6 .



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@3945 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/build.xml b/dev/core/build.xml
index bcc6f13..e5090e6 100755
--- a/dev/core/build.xml
+++ b/dev/core/build.xml
@@ -26,6 +26,7 @@
       <zipfileset src="${gwt.tools.lib}/apache/tapestry-util-text-4.0.2.jar" />
       <zipfileset src="${gwt.tools.lib}/apache/ant-1.6.5.jar" />
       <zipfileset src="${gwt.tools.lib}/eclipse/jdt-3.3.1.jar" />
+      <zipfileset src="${gwt.tools.lib}/jetty/jetty-6.1.11.jar" />
       <zipfileset src="${gwt.tools.lib}/tomcat/ant-launcher-1.6.5.jar" />
       <zipfileset src="${gwt.tools.lib}/tomcat/catalina-1.0.jar" />
       <zipfileset src="${gwt.tools.lib}/tomcat/catalina-optional-1.0.jar" />
diff --git a/dev/core/src/com/google/gwt/core/ext/Linker.java b/dev/core/src/com/google/gwt/core/ext/Linker.java
index 5cce5e6..5dc6ff1 100644
--- a/dev/core/src/com/google/gwt/core/ext/Linker.java
+++ b/dev/core/src/com/google/gwt/core/ext/Linker.java
@@ -23,6 +23,19 @@
  * the relative ordering of the Linkers. Exact order of Linker execution will be
  * determined by the order of <code>add-linker</code> tags in the module
  * configuration.
+ * 
+ * <p>
+ * A new instance of a linker is created each time a module is compiled or
+ * during hosted mode when a module first loads (or is refreshed). During a
+ * compile, {@link #link(TreeLogger, LinkerContext, ArtifactSet)} will be called
+ * exactly once, and the artifact set will contain any and all generated
+ * artifacts. . In hosted mode,
+ * {@link #link(TreeLogger, LinkerContext, ArtifactSet)} is called initially,
+ * but with no generated artifacts. If any artifacts are subsequently generated
+ * during the course of running hosted mode,
+ * {@link #relink(TreeLogger, LinkerContext, ArtifactSet)} will be called with
+ * the new artifacts.
+ * </p>
  */
 public abstract class Linker {
   /**
@@ -42,4 +55,26 @@
    */
   public abstract ArtifactSet link(TreeLogger logger, LinkerContext context,
       ArtifactSet artifacts) throws UnableToCompleteException;
+
+  /**
+   * Re-invoke the Linker with newly generated artifacts. Linkers that need to
+   * reference the original artifact set passed into
+   * {@link #link(TreeLogger, LinkerContext, ArtifactSet)} should retain a copy
+   * of the original artifact set in an instance variable.
+   * 
+   * @param logger the TreeLogger to record to
+   * @param context provides access to the Linker's environment
+   * @param newArtifacts an unmodifiable view of the newly generated artifacts
+   * @return the new artifacts that should be propagated through the linker
+   *         chain; it is not necessary to return any artifacts from the
+   *         original link (or previous calls to relink) that have not been
+   *         modified
+   * @throws UnableToCompleteException if compilation violates assumptions made
+   *           by the Linker or for errors encountered by the Linker
+   */
+  @SuppressWarnings("unused")
+  public ArtifactSet relink(TreeLogger logger, LinkerContext context,
+      ArtifactSet newArtifacts) throws UnableToCompleteException {
+    return newArtifacts;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java b/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java
index 96435bc..6e865c0 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/ArtifactSet.java
@@ -177,4 +177,9 @@
   public <T> T[] toArray(T[] a) {
     return treeSet.toArray(a);
   }
+
+  @Override
+  public String toString() {
+    return treeSet.toString();
+  }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java b/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
index 85c4ed2..96bc46b 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/EmittedArtifact.java
@@ -27,6 +27,9 @@
  * be emitted by the compiler into the module's output directory. This type may
  * be extended by Linker providers to provide alternative implementations of
  * {@link #getContents(TreeLogger)}.
+ * 
+ * TODO(bobv): provide a timestamp so we can make the time on output files match
+ * that of input files?
  */
 public abstract class EmittedArtifact extends Artifact<EmittedArtifact> {
 
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
index bb2a27d..26353b1 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
@@ -59,12 +59,10 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.SortedSet;
-import java.util.Stack;
 import java.util.TreeSet;
 
 /**
@@ -114,7 +112,6 @@
 
   private final SortedSet<ConfigurationProperty> configurationProperties;
   private final JJSOptions jjsOptions;
-  private final List<Class<? extends Linker>> linkerClasses;
   private final Map<Class<? extends Linker>, String> linkerShortNames = new HashMap<Class<? extends Linker>, String>();
 
   private final String moduleFunctionName;
@@ -124,17 +121,59 @@
   private final Map<String, StandardCompilationResult> resultsByStrongName = new HashMap<String, StandardCompilationResult>();
   private final SortedSet<SelectionProperty> selectionProperties;
 
+  private final Linker[] linkers;
+
   public StandardLinkerContext(TreeLogger logger, ModuleDef module,
-      JJSOptions jjsOptions) {
+      JJSOptions jjsOptions) throws UnableToCompleteException {
     logger = logger.branch(TreeLogger.DEBUG,
         "Constructing StandardLinkerContext", null);
 
     this.jjsOptions = jjsOptions;
     this.moduleFunctionName = module.getFunctionName();
     this.moduleName = module.getName();
-    this.linkerClasses = new ArrayList<Class<? extends Linker>>(
-        module.getActiveLinkers());
-    linkerClasses.add(module.getActivePrimaryLinker());
+
+    // Sort the linkers into the order they should actually run.
+    List<Class<? extends Linker>> sortedLinkers = new ArrayList<Class<? extends Linker>>();
+
+    // Get all the pre-linkers first.
+    for (Class<? extends Linker> linkerClass : module.getActiveLinkers()) {
+      Order order = linkerClass.getAnnotation(LinkerOrder.class).value();
+      assert (order != null);
+      if (order == Order.PRE) {
+        sortedLinkers.add(linkerClass);
+      }
+    }
+
+    // Get the primary linker.
+    sortedLinkers.add(module.getActivePrimaryLinker());
+
+    // Get all the post-linkers IN REVERSE ORDER.
+    {
+      List<Class<? extends Linker>> postLinkerClasses = new ArrayList<Class<? extends Linker>>();
+      for (Class<? extends Linker> linkerClass : module.getActiveLinkers()) {
+        Order order = linkerClass.getAnnotation(LinkerOrder.class).value();
+        assert (order != null);
+        if (order == Order.POST) {
+          postLinkerClasses.add(linkerClass);
+        }
+      }
+      Collections.reverse(postLinkerClasses);
+      sortedLinkers.addAll(postLinkerClasses);
+    }
+
+    linkers = new Linker[sortedLinkers.size()];
+    int i = 0;
+    for (Class<? extends Linker> linkerClass : sortedLinkers) {
+      try {
+        linkers[i++] = linkerClass.newInstance();
+      } catch (InstantiationException e) {
+        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
+        throw new UnableToCompleteException();
+      } catch (IllegalAccessException e) {
+        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
+        throw new UnableToCompleteException();
+      }
+    }
 
     for (Map.Entry<String, Class<? extends Linker>> entry : module.getLinkers().entrySet()) {
       linkerShortNames.put(entry.getValue(), entry.getKey());
@@ -271,77 +310,39 @@
   /**
    * Run the linker stack.
    */
-  public ArtifactSet invokeLinkerStack(TreeLogger logger)
+  public ArtifactSet invokeLink(TreeLogger logger)
       throws UnableToCompleteException {
     ArtifactSet workingArtifacts = new ArtifactSet(artifacts);
-    Stack<Linker> linkerStack = new Stack<Linker>();
 
-    EnumSet<Order> phasePre = EnumSet.of(Order.PRE, Order.PRIMARY);
-    EnumSet<Order> phasePost = EnumSet.of(Order.POST);
-
-    // Instantiate instances of the Linkers
-    for (Class<? extends Linker> clazz : linkerClasses) {
-      Linker linker;
-
-      // Create an instance of the Linker
-      try {
-        linker = clazz.newInstance();
-        linkerStack.push(linker);
-      } catch (InstantiationException e) {
-        logger.log(TreeLogger.ERROR, "Unable to create LinkerContextShim", e);
-        throw new UnableToCompleteException();
-      } catch (IllegalAccessException e) {
-        logger.log(TreeLogger.ERROR, "Unable to create LinkerContextShim", e);
-        throw new UnableToCompleteException();
-      }
-
-      // Detemine if we need to invoke the Linker in the current link phase
-      Order order = clazz.getAnnotation(LinkerOrder.class).value();
-      if (!phasePre.contains(order)) {
-        continue;
-      }
-
-      // The primary Linker is guaranteed to be last in the order
-      if (order == Order.PRIMARY) {
-        assert linkerClasses.get(linkerClasses.size() - 1).equals(clazz);
-      }
-
+    for (Linker linker : linkers) {
       TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
           "Invoking Linker " + linker.getDescription(), null);
-
       workingArtifacts.freeze();
       try {
         workingArtifacts = linker.link(linkerLogger, this, workingArtifacts);
-      } catch (Exception e) {
+      } catch (Throwable e) {
         linkerLogger.log(TreeLogger.ERROR, "Failed to link", e);
         throw new UnableToCompleteException();
       }
     }
+    return workingArtifacts;
+  }
 
-    // Pop the primary linker off of the stack
-    linkerStack.pop();
+  public ArtifactSet invokeRelink(TreeLogger logger,
+      ArtifactSet newlyGeneratedArtifacts) throws UnableToCompleteException {
+    ArtifactSet workingArtifacts = new ArtifactSet(newlyGeneratedArtifacts);
 
-    // Unwind the stack
-    while (!linkerStack.isEmpty()) {
-      Linker linker = linkerStack.pop();
-      Class<? extends Linker> linkerType = linker.getClass();
-
-      // See if the Linker should be run in the current phase
-      Order order = linkerType.getAnnotation(LinkerOrder.class).value();
-      if (phasePost.contains(order)) {
-        TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
-            "Invoking Linker " + linker.getDescription(), null);
-
-        workingArtifacts.freeze();
-        try {
-          workingArtifacts = linker.link(linkerLogger, this, workingArtifacts);
-        } catch (Exception e) {
-          linkerLogger.log(TreeLogger.ERROR, "Failed to link", e);
-          throw new UnableToCompleteException();
-        }
+    for (Linker linker : linkers) {
+      TreeLogger linkerLogger = logger.branch(TreeLogger.TRACE,
+          "Invoking relink on Linker " + linker.getDescription(), null);
+      workingArtifacts.freeze();
+      try {
+        workingArtifacts = linker.relink(linkerLogger, this, workingArtifacts);
+      } catch (Throwable e) {
+        linkerLogger.log(TreeLogger.ERROR, "Failed to relink", e);
+        throw new UnableToCompleteException();
       }
     }
-
     return workingArtifacts;
   }
 
@@ -414,11 +415,20 @@
     return out.toString();
   }
 
+  /**
+   * Writes artifacts into output directories in the standard way.
+   * 
+   * @param logger logs the operation
+   * @param artifacts the set of artifacts to write
+   * @param outputPath the output path for deployable artifacts
+   * @param extraPath optional extra path for non-deployable artifacts
+   * @throws UnableToCompleteException
+   */
   public void produceOutputDirectory(TreeLogger logger, ArtifactSet artifacts,
-      File moduleOutDir, File moduleAuxDir) throws UnableToCompleteException {
+      File outputPath, File extraPath) throws UnableToCompleteException {
 
-    logger = logger.branch(TreeLogger.INFO, "Linking compilation into "
-        + moduleOutDir.getPath(), null);
+    logger = logger.branch(TreeLogger.TRACE, "Linking compilation into "
+        + outputPath.getPath(), null);
 
     for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
       TreeLogger artifactLogger = logger.branch(TreeLogger.DEBUG,
@@ -426,14 +436,19 @@
 
       File outFile;
       if (artifact.isPrivate()) {
-        outFile = new File(getLinkerAuxDir(moduleAuxDir, artifact.getLinker()),
-            artifact.getPartialPath());
+        if (extraPath == null) {
+          continue;
+        }
+        outFile = new File(getExtraPathForLinker(extraPath,
+            artifact.getLinker()), artifact.getPartialPath());
       } else {
-        outFile = new File(moduleOutDir, artifact.getPartialPath());
+        outFile = new File(outputPath, artifact.getPartialPath());
       }
 
-      assert !outFile.exists() : "Attempted to overwrite " + outFile.getPath();
-      Util.copy(logger, artifact.getContents(artifactLogger), outFile);
+      // TODO(scottb): figure out how to do a clean.
+      // assert !outFile.exists() : "Attempted to overwrite " +
+      // outFile.getPath();
+      Util.copy(artifactLogger, artifact.getContents(artifactLogger), outFile);
     }
   }
 
@@ -441,15 +456,11 @@
    * Creates a linker-specific subdirectory in the module's auxiliary output
    * directory.
    */
-  private File getLinkerAuxDir(File moduleAuxDir,
+  private File getExtraPathForLinker(File extraPath,
       Class<? extends Linker> linkerType) {
-    // The auxiliary directory is create lazily
-    if (!moduleAuxDir.exists()) {
-      moduleAuxDir.mkdirs();
-    }
     assert linkerShortNames.containsKey(linkerType) : linkerType.getName()
         + " unknown";
-    File toReturn = new File(moduleAuxDir, linkerShortNames.get(linkerType));
+    File toReturn = new File(extraPath, linkerShortNames.get(linkerType));
     if (!toReturn.exists()) {
       toReturn.mkdirs();
     }
diff --git a/dev/core/src/com/google/gwt/dev/CompileArgProcessor.java b/dev/core/src/com/google/gwt/dev/CompileArgProcessor.java
new file mode 100644
index 0000000..d19ae45
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/CompileArgProcessor.java
@@ -0,0 +1,42 @@
+/*
+ * 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.dev.util.arg.ArgHandlerLogLevel;
+import com.google.gwt.dev.util.arg.ArgHandlerModuleName;
+import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
+import com.google.gwt.dev.util.arg.ArgHandlerTreeLoggerFlag;
+import com.google.gwt.util.tools.ToolBase;
+
+abstract class CompileArgProcessor extends ToolBase {
+  public CompileArgProcessor(CompileTaskOptions options) {
+    registerHandler(new ArgHandlerLogLevel(options));
+    registerHandler(new ArgHandlerTreeLoggerFlag(options));
+    registerHandler(new ArgHandlerOutDir(options));
+    registerHandler(new ArgHandlerModuleName(options));
+  }
+
+  /*
+   * Overridden to make public.
+   */
+  @Override
+  public final boolean processArgs(String[] args) {
+    return super.processArgs(args);
+  }
+
+  @Override
+  protected abstract String getName();
+}
diff --git a/dev/core/src/com/google/gwt/dev/CompilePerms.java b/dev/core/src/com/google/gwt/dev/CompilePerms.java
index 6ead2f6..12faf69 100644
--- a/dev/core/src/com/google/gwt/dev/CompilePerms.java
+++ b/dev/core/src/com/google/gwt/dev/CompilePerms.java
@@ -116,7 +116,8 @@
       return true;
     }
   }
-  static class ArgProcessor extends Link.ArgProcessor {
+
+  static class ArgProcessor extends CompileArgProcessor {
     public ArgProcessor(CompilePermsOptions options) {
       super(options);
       registerHandler(new ArgHandlerPerms(options));
@@ -129,7 +130,7 @@
   }
 
   /**
-   * Concrete class to implement all compiler options.
+   * Concrete class to implement compiler perm options.
    */
   static class CompilePermsOptionsImpl extends CompileTaskOptionsImpl implements
       CompilePermsOptions {
diff --git a/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java b/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
index b2f0295..9c093c0 100644
--- a/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
+++ b/dev/core/src/com/google/gwt/dev/CompileTaskOptionsImpl.java
@@ -24,14 +24,13 @@
  */
 class CompileTaskOptionsImpl implements CompileTaskOptions {
 
-  public static final String GWT_COMPILER_DIR = ".gwt-tmp" + File.separatorChar
-      + "compiler";
+  public static final String GWT_TMP_DIR = "gwt-tmp";
 
-  private File compilerWorkDir;
   private Type logLevel;
   private String moduleName;
   private File outDir;
   private boolean useGuiLogger;
+  private File workDir;
 
   public CompileTaskOptionsImpl() {
   }
@@ -48,11 +47,7 @@
   }
 
   public File getCompilerWorkDir() {
-    if (compilerWorkDir == null) {
-      compilerWorkDir = new File(getOutDir(), GWT_COMPILER_DIR + File.separator
-          + moduleName);
-    }
-    return compilerWorkDir;
+    return new File(new File(getWorkDir(), getModuleName()), "compiler");
   }
 
   public Type getLogLevel() {
@@ -87,4 +82,14 @@
     this.useGuiLogger = useGuiLogger;
   }
 
-}
\ No newline at end of file
+  /**
+   * TODO: add a command line option to pass files between compile phases?
+   */
+  protected File getWorkDir() {
+    if (workDir == null) {
+      workDir = new File(System.getProperty("java.io.tmpdir"), GWT_TMP_DIR);
+      workDir.mkdirs();
+    }
+    return workDir;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/CompilerOptions.java b/dev/core/src/com/google/gwt/dev/CompilerOptions.java
index 85b4afc..0b568e4 100644
--- a/dev/core/src/com/google/gwt/dev/CompilerOptions.java
+++ b/dev/core/src/com/google/gwt/dev/CompilerOptions.java
@@ -15,13 +15,11 @@
  */
 package com.google.gwt.dev;
 
-import com.google.gwt.dev.jjs.JJSOptions;
-import com.google.gwt.dev.util.arg.OptionGenDir;
-import com.google.gwt.dev.util.arg.OptionValidateOnly;
+import com.google.gwt.dev.Link.LinkOptions;
+import com.google.gwt.dev.Precompile.PrecompileOptions;
 
 /**
  * The complete set of options for the GWT compiler.
  */
-public interface CompilerOptions extends JJSOptions, CompileTaskOptions,
-    OptionGenDir, OptionValidateOnly {
+public interface CompilerOptions extends PrecompileOptions, LinkOptions {
 }
diff --git a/dev/core/src/com/google/gwt/dev/CompilerOptionsImpl.java b/dev/core/src/com/google/gwt/dev/CompilerOptionsImpl.java
deleted file mode 100644
index dcc29bf..0000000
--- a/dev/core/src/com/google/gwt/dev/CompilerOptionsImpl.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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.Type;
-import com.google.gwt.dev.jjs.JJSOptionsImpl;
-
-import java.io.File;
-
-/**
- * Concrete class to implement all compiler options.
- */
-public class CompilerOptionsImpl extends JJSOptionsImpl implements
-    CompilerOptions {
-  private File genDir;
-  private Type logLevel;
-  private String moduleName;
-  private File outDir;
-  private boolean useGuiLogger;
-  private boolean validateOnly;
-
-  public CompilerOptionsImpl() {
-  }
-
-  public CompilerOptionsImpl(CompilerOptions other) {
-    copyFrom(other);
-  }
-
-  public void copyFrom(CompilerOptions other) {
-    super.copyFrom(other);
-    setGenDir(other.getGenDir());
-    setLogLevel(other.getLogLevel());
-    setOutDir(other.getOutDir());
-    setUseGuiLogger(other.isUseGuiLogger());
-    setValidateOnly(false);
-  }
-
-  public File getGenDir() {
-    return genDir;
-  }
-
-  public Type getLogLevel() {
-    return logLevel;
-  }
-
-  public String getModuleName() {
-    return moduleName;
-  }
-
-  public File getOutDir() {
-    return outDir;
-  }
-
-  public boolean isUseGuiLogger() {
-    return useGuiLogger;
-  }
-
-  public boolean isValidateOnly() {
-    return validateOnly;
-  }
-
-  public void setGenDir(File genDir) {
-    this.genDir = genDir;
-  }
-
-  public void setLogLevel(Type logLevel) {
-    this.logLevel = logLevel;
-  }
-
-  public void setModuleName(String moduleName) {
-    this.moduleName = moduleName;
-  }
-
-  public void setOutDir(File outDir) {
-    this.outDir = outDir;
-  }
-
-  public void setUseGuiLogger(boolean useGuiLogger) {
-    this.useGuiLogger = useGuiLogger;
-  }
-
-  public void setValidateOnly(boolean validateOnly) {
-    this.validateOnly = validateOnly;
-  }
-}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/GWTCompiler.java b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
index 3f89bfd..9e90603 100644
--- a/dev/core/src/com/google/gwt/dev/GWTCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/GWTCompiler.java
@@ -19,7 +19,10 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.CompilePerms.CompilePermsOptionsImpl;
 import com.google.gwt.dev.CompileTaskRunner.CompileTask;
-import com.google.gwt.dev.Precompile.CompilerOptionsImpl;
+import com.google.gwt.dev.Precompile.PrecompileOptionsImpl;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
+
+import java.io.File;
 
 /**
  * The main executable entry point for the GWT Java to JavaScript compiler.
@@ -29,6 +32,7 @@
   static final class ArgProcessor extends Precompile.ArgProcessor {
     public ArgProcessor(CompilerOptions options) {
       super(options);
+      registerHandler(new ArgHandlerExtraDir(options));
     }
 
     @Override
@@ -37,6 +41,32 @@
     }
   }
 
+  static class GWTCompilerOptionsImpl extends PrecompileOptionsImpl implements
+      CompilerOptions {
+
+    private File extraDir;
+
+    public GWTCompilerOptionsImpl() {
+    }
+
+    public GWTCompilerOptionsImpl(CompilerOptions other) {
+      copyFrom(other);
+    }
+
+    public void copyFrom(CompilerOptions other) {
+      super.copyFrom(other);
+      setExtraDir(other.getExtraDir());
+    }
+
+    public File getExtraDir() {
+      return extraDir;
+    }
+
+    public void setExtraDir(File extraDir) {
+      this.extraDir = extraDir;
+    }
+  }
+
   public static void main(String[] args) {
     /*
      * NOTE: main always exits with a call to System.exit to terminate any
@@ -44,7 +74,7 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    final CompilerOptions options = new CompilerOptionsImpl();
+    final CompilerOptions options = new GWTCompilerOptionsImpl();
     if (new ArgProcessor(options).processArgs(args)) {
       CompileTask task = new CompileTask() {
         public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -60,10 +90,10 @@
     System.exit(1);
   }
 
-  private final CompilerOptionsImpl options;
+  private final GWTCompilerOptionsImpl options;
 
   public GWTCompiler(CompilerOptions options) {
-    this.options = new CompilerOptionsImpl(options);
+    this.options = new GWTCompilerOptionsImpl(options);
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
diff --git a/dev/core/src/com/google/gwt/dev/GWTHosted.java b/dev/core/src/com/google/gwt/dev/GWTHosted.java
new file mode 100644
index 0000000..f81cb76
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/GWTHosted.java
@@ -0,0 +1,188 @@
+/*
+ * 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.linker.ArtifactSet;
+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.GWTShellServletFilter;
+import com.google.gwt.dev.shell.ServletContainer;
+import com.google.gwt.dev.shell.jetty.JettyLauncher;
+import com.google.gwt.dev.util.PerfLogger;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.util.tools.ArgHandlerExtra;
+import com.google.gwt.util.tools.ArgHandlerString;
+
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The main executable class for the hosted mode shell.
+ */
+public class GWTHosted extends GWTShell {
+
+  /**
+   * Handles the set of modules that can be passed at the end of the command
+   * line.
+   */
+  protected class ArgHandlerModulesExtra extends ArgHandlerExtra {
+
+    private final PrintWriterTreeLogger console = new PrintWriterTreeLogger(
+        new PrintWriter(System.err));
+    {
+      console.setMaxDetail(TreeLogger.WARN);
+    }
+
+    @Override
+    public boolean addExtraArg(String arg) {
+      return addModule(console, arg);
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Specifies the set of modules to host";
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"module"};
+    }
+  }
+
+  /**
+   * Handles a startup url that can be passed on the command line.
+   */
+  protected class ArgHandlerStartupURLs extends ArgHandlerString {
+
+    @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) {
+      addStartupURL(arg);
+      return true;
+    }
+  }
+
+  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.
+     */
+    BootStrapPlatform.init();
+    GWTHosted shellMain = new GWTHosted();
+    if (shellMain.processArgs(args)) {
+      shellMain.run();
+    }
+    System.exit(0);
+  }
+
+  private Set<ModuleDef> modules = new HashSet<ModuleDef>();
+
+  private ServletContainer server;
+
+  private GWTShellServletFilter servletFilter;
+
+  public GWTHosted() {
+    super(false, true);
+    registerHandler(new ArgHandlerStartupURLs());
+    registerHandler(new ArgHandlerModulesExtra());
+  }
+
+  public boolean addModule(TreeLogger logger, String moduleName) {
+    try {
+      ModuleDef moduleDef = ModuleDefLoader.loadFromClassPath(logger,
+          moduleName);
+      modules.add(moduleDef);
+      return true;
+    } catch (UnableToCompleteException e) {
+      System.err.println("Unable to load module '" + moduleName + "'");
+      return false;
+    }
+  }
+
+  @Override
+  protected ArtifactAcceptor doCreateArtifactAcceptor(final ModuleDef module) {
+    return new ArtifactAcceptor() {
+      public void accept(TreeLogger logger, ArtifactSet newlyGeneratedArtifacts)
+          throws UnableToCompleteException {
+        servletFilter.relink(logger, module, newlyGeneratedArtifacts);
+      }
+    };
+  }
+
+  @Override
+  protected void shutDown() {
+    if (server != null) {
+      try {
+        server.stop();
+      } catch (UnableToCompleteException e) {
+        // Already logged.
+      }
+      server = null;
+    }
+  }
+
+  @Override
+  protected int startUpServer() {
+    PerfLogger.start("GWTShell.startup (Jetty launch)");
+    JettyLauncher launcher = new JettyLauncher();
+    try {
+      TreeLogger serverLogger = getTopLogger().branch(TreeLogger.INFO,
+          "Starting HTTP on port " + getPort(), null);
+      ModuleDef[] moduleArray = modules.toArray(new ModuleDef[modules.size()]);
+      for (ModuleDef moduleDef : moduleArray) {
+        String[] servletPaths = moduleDef.getServletPaths();
+        if (servletPaths.length > 0) {
+          serverLogger.log(TreeLogger.WARN,
+              "Ignoring legacy <servlet> tag(s) in module '"
+                  + moduleDef.getName()
+                  + "'; add servlet tags to your web.xml instead");
+        }
+      }
+      servletFilter = new GWTShellServletFilter(serverLogger, options,
+          moduleArray);
+      server = launcher.start(serverLogger, getPort(), options.getOutDir(),
+          servletFilter);
+    } catch (UnableToCompleteException e) {
+      PerfLogger.end();
+      return -1;
+    }
+    assert (server != null);
+
+    PerfLogger.end();
+    return server.getPort();
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/GWTShell.java b/dev/core/src/com/google/gwt/dev/GWTShell.java
index 7a56d3d..2da8519 100644
--- a/dev/core/src/com/google/gwt/dev/GWTShell.java
+++ b/dev/core/src/com/google/gwt/dev/GWTShell.java
@@ -18,10 +18,13 @@
 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.Precompile.CompilerOptionsImpl;
+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.BrowserWidget;
 import com.google.gwt.dev.shell.BrowserWidgetHost;
 import com.google.gwt.dev.shell.BrowserWidgetHostChecker;
@@ -30,10 +33,12 @@
 import com.google.gwt.dev.shell.PlatformSpecific;
 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.PerfLogger;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.arg.ArgHandlerDisableAggressiveOptimization;
 import com.google.gwt.dev.util.arg.ArgHandlerEnableAssertions;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
 import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
 import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
 import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
@@ -160,9 +165,10 @@
   }
 
   /**
-   * Handles the list of startup urls that can be passed on the command line.
+   * Handles the list of startup urls that can be passed at the end of the
+   * command line.
    */
-  protected class ArgHandlerStartupURLs extends ArgHandlerExtra {
+  protected class ArgHandlerStartupURLsExtra extends ArgHandlerExtra {
 
     @Override
     public boolean addExtraArg(String arg) {
@@ -212,6 +218,35 @@
     }
   }
 
+  /**
+   * Concrete class to implement all compiler options.
+   */
+  static class ShellOptionsImpl extends GWTCompilerOptionsImpl implements
+      ShellOptions, WorkDirs {
+    public File getCompilerOutputDir(ModuleDef moduleDef) {
+      return new File(getOutDir(), moduleDef.getDeployTo());
+    }
+
+    public File getShellPublicGenDir(ModuleDef moduleDef) {
+      return new File(getShellBaseWorkDir(moduleDef), "public");
+    }
+
+    /**
+     * The base shell work directory.
+     */
+    protected File getShellBaseWorkDir(ModuleDef moduleDef) {
+      return new File(new File(getWorkDir(), moduleDef.getName()), "shell");
+    }
+
+    /**
+     * Where generated files go by default until we are sure they are public;
+     * then they are copied into {@link #getShellPublicGenDir(ModuleDef)}.
+     */
+    protected File getShellPrivateGenDir(ModuleDef moduleDef) {
+      return new File(getShellBaseWorkDir(moduleDef), "gen");
+    }
+  }
+
   private class BrowserWidgetHostImpl implements BrowserWidgetHost {
     public BrowserWidgetHostImpl() {
     }
@@ -244,14 +279,9 @@
         ModuleDef moduleDef = loadModule(moduleName, logger);
         assert (moduleDef != null);
 
-        // Create a sandbox for the module.
-        //
-        File shellDir = new File(options.getOutDir(), GWT_SHELL_PATH
-            + File.separator + moduleName);
-
         TypeOracle typeOracle = moduleDef.getTypeOracle(logger);
         ShellModuleSpaceHost host = doCreateShellModuleSpaceHost(logger,
-            typeOracle, moduleDef, options.getGenDir(), shellDir);
+            typeOracle, moduleDef);
         return host;
       } finally {
         Cursor normalCursor = display.getSystemCursor(SWT.CURSOR_ARROW);
@@ -291,9 +321,6 @@
     }
   }
 
-  public static final String GWT_SHELL_PATH = ".gwt-tmp" + File.separator
-      + "shell";
-
   private static Image[] icons;
 
   static {
@@ -314,7 +341,7 @@
   }
 
   public static String computeHostRegex(String url) {
-    // the enture URL up to the first slash not prefixed by a slash or colon.
+    // the entire URL up to the first slash not prefixed by a slash or colon.
     String raw = url.split("(?<![:/])/")[0];
     // escape the dots and put a begin line specifier on the result
     return "^" + raw.replaceAll("[.]", "[.]");
@@ -365,6 +392,8 @@
    */
   protected final Display display = Display.getDefault();
 
+  protected final ShellOptionsImpl options = new ShellOptionsImpl();
+
   /**
    * Cheat on the first load's refresh by assuming the module loaded by
    * {@link com.google.gwt.dev.shell.GWTShellServlet} is still fresh. This
@@ -381,8 +410,6 @@
 
   private ShellMainWindow mainWnd;
 
-  private final CompilerOptionsImpl options = new CompilerOptionsImpl();
-
   private int port;
 
   private boolean runTomcat = true;
@@ -408,17 +435,16 @@
     registerHandler(new ArgHandlerLogLevel(options));
 
     registerHandler(new ArgHandlerGenDir(options));
+    registerHandler(new ArgHandlerExtraDir(options));
 
     if (!noURLs) {
-      registerHandler(new ArgHandlerStartupURLs());
+      registerHandler(new ArgHandlerStartupURLsExtra());
     }
 
     registerHandler(new ArgHandlerOutDir(options));
 
     registerHandler(new ArgHandlerScriptStyle(options));
-
     registerHandler(new ArgHandlerEnableAssertions(options));
-
     registerHandler(new ArgHandlerDisableAggressiveOptimization(options));
   }
 
@@ -433,7 +459,7 @@
   }
 
   public CompilerOptions getCompilerOptions() {
-    return new CompilerOptionsImpl(options);
+    return new GWTCompilerOptionsImpl(options);
   }
 
   public int getPort() {
@@ -578,11 +604,31 @@
    */
   protected void compile(TreeLogger logger, ModuleDef moduleDef)
       throws UnableToCompleteException {
-    CompilerOptions newOptions = new CompilerOptionsImpl(options);
+    CompilerOptions newOptions = new GWTCompilerOptionsImpl(options);
     newOptions.setModuleName(moduleDef.getName());
     new GWTCompiler(newOptions).run(logger);
   }
 
+  protected ArtifactAcceptor doCreateArtifactAcceptor(final ModuleDef module) {
+    return new ArtifactAcceptor() {
+      public void accept(TreeLogger logger, ArtifactSet artifacts)
+          throws UnableToCompleteException {
+
+        /*
+         * Copied from StandardLinkerContext.produceOutputDirectory() for legacy
+         * GWTShellServlet support.
+         */
+        for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
+          if (!artifact.isPrivate()) {
+            File outFile = new File(options.getShellPublicGenDir(module),
+                artifact.getPartialPath());
+            Util.copy(logger, artifact.getContents(logger), outFile);
+          }
+        }
+      }
+    };
+  }
+
   /**
    * Creates an instance of ShellModuleSpaceHost (or a derived class) using the
    * specified constituent parts. This method is made to be overridden for
@@ -595,10 +641,12 @@
    * @return ShellModuleSpaceHost instance
    */
   protected ShellModuleSpaceHost doCreateShellModuleSpaceHost(
-      TreeLogger logger, TypeOracle typeOracle, ModuleDef moduleDef,
-      File genDir, File shellDir) {
-    return new ShellModuleSpaceHost(logger, typeOracle, moduleDef, genDir,
-        shellDir);
+      TreeLogger logger, TypeOracle typeOracle, ModuleDef moduleDef) {
+    // Clear out the shell temp directory.
+    Util.recursiveDelete(options.getShellBaseWorkDir(moduleDef), true);
+    return new ShellModuleSpaceHost(logger, typeOracle, moduleDef,
+        options.getGenDir(), options.getShellPrivateGenDir(moduleDef),
+        doCreateArtifactAcceptor(moduleDef));
   }
 
   /**
@@ -712,29 +760,30 @@
     initializeLogger();
 
     if (runTomcat) {
-      // Start the HTTP server.
-      // Use a new thread so that logging that occurs during startup is
-      // displayed immediately.
-      //
-      final int serverPort = getPort();
-
-      PerfLogger.start("GWTShell.startup (Tomcat launch)");
-      String whyFailed = EmbeddedTomcatServer.start(getTopLogger(), serverPort,
-          options.getOutDir());
-      PerfLogger.end();
-
-      if (whyFailed != null) {
-        System.err.println(whyFailed);
+      int resultPort = startUpServer();
+      if (resultPort < 0) {
         return false;
       }
-
-      // Record what port Tomcat is actually running on.
-      port = EmbeddedTomcatServer.getPort();
+      port = resultPort;
     }
 
     return true;
   }
 
+  protected int startUpServer() {
+    // TODO(bruce): make tomcat work in terms of the modular launcher
+    String whyFailed = EmbeddedTomcatServer.start(getTopLogger(), 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);
+      return -1;
+    }
+    return EmbeddedTomcatServer.getPort();
+  }
+
   private Shell createTrackedBrowserShell() {
     final Shell shell = new Shell(display);
     FillLayout fillLayout = new FillLayout();
diff --git a/dev/core/src/com/google/gwt/dev/Link.java b/dev/core/src/com/google/gwt/dev/Link.java
index 2bc55cd..d458108 100644
--- a/dev/core/src/com/google/gwt/dev/Link.java
+++ b/dev/core/src/com/google/gwt/dev/Link.java
@@ -27,11 +27,8 @@
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.cfg.StaticPropertyOracle;
 import com.google.gwt.dev.util.Util;
-import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
-import com.google.gwt.dev.util.arg.ArgHandlerModuleName;
-import com.google.gwt.dev.util.arg.ArgHandlerOutDir;
-import com.google.gwt.dev.util.arg.ArgHandlerTreeLoggerFlag;
-import com.google.gwt.util.tools.ToolBase;
+import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
+import com.google.gwt.dev.util.arg.OptionExtraDir;
 
 import java.io.File;
 import java.util.HashMap;
@@ -42,21 +39,16 @@
  * to compile, and a ready-to-compile AST.
  */
 public class Link {
+  /**
+   * Options for Link.
+   */
+  public interface LinkOptions extends CompileTaskOptions, OptionExtraDir {
+  }
 
-  static class ArgProcessor extends ToolBase {
-    public ArgProcessor(CompileTaskOptions options) {
-      registerHandler(new ArgHandlerLogLevel(options));
-      registerHandler(new ArgHandlerTreeLoggerFlag(options));
-      registerHandler(new ArgHandlerOutDir(options));
-      registerHandler(new ArgHandlerModuleName(options));
-    }
-
-    /*
-     * Overridden to make public.
-     */
-    @Override
-    public boolean processArgs(String[] args) {
-      return super.processArgs(args);
+  static class ArgProcessor extends CompileArgProcessor {
+    public ArgProcessor(LinkOptions options) {
+      super(options);
+      registerHandler(new ArgHandlerExtraDir(options));
     }
 
     @Override
@@ -65,6 +57,35 @@
     }
   }
 
+  /**
+   * Concrete class to implement link options.
+   */
+  static class LinkOptionsImpl extends CompileTaskOptionsImpl implements
+      LinkOptions {
+
+    private File extraDir;
+
+    public LinkOptionsImpl() {
+    }
+
+    public LinkOptionsImpl(LinkOptions other) {
+      copyFrom(other);
+    }
+
+    public void copyFrom(LinkOptions other) {
+      super.copyFrom(other);
+      setExtraDir(other.getExtraDir());
+    }
+
+    public File getExtraDir() {
+      return extraDir;
+    }
+
+    public void setExtraDir(File extraDir) {
+      this.extraDir = extraDir;
+    }
+  }
+
   public static ArtifactSet link(TreeLogger logger, ModuleDef module,
       Precompilation precompilation, File[] jsFiles)
       throws UnableToCompleteException {
@@ -80,7 +101,7 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    final CompileTaskOptions options = new CompileTaskOptionsImpl();
+    final LinkOptions options = new LinkOptionsImpl();
     if (new ArgProcessor(options).processArgs(args)) {
       CompileTask task = new CompileTask() {
         public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -110,7 +131,7 @@
     }
 
     linkerContext.addOrReplaceArtifacts(precompilation.getGeneratedArtifacts());
-    return linkerContext.invokeLinkerStack(logger);
+    return linkerContext.invokeLink(logger);
   }
 
   private static void finishPermuation(TreeLogger logger, Permutation perm,
@@ -143,17 +164,17 @@
   /**
    * This is the output directory for private files.
    */
-  private File moduleAuxDir;
+  private File moduleExtraDir;
 
   /**
    * This is the output directory for public files.
    */
   private File moduleOutDir;
 
-  private final CompileTaskOptionsImpl options;
+  private final LinkOptionsImpl options;
 
-  public Link(CompileTaskOptions options) {
-    this.options = new CompileTaskOptionsImpl(options);
+  public Link(LinkOptions options) {
+    this.options = new LinkOptionsImpl(options);
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -197,7 +218,7 @@
         jsFiles);
     if (artifacts != null) {
       linkerContext.produceOutputDirectory(branch, artifacts, moduleOutDir,
-          moduleAuxDir);
+          moduleExtraDir);
       branch.log(TreeLogger.INFO, "Link succeeded");
       return true;
     }
@@ -207,9 +228,11 @@
 
   private void init(TreeLogger logger) throws UnableToCompleteException {
     module = ModuleDefLoader.loadFromClassPath(logger, options.getModuleName());
-    moduleOutDir = new File(options.getOutDir(), module.getName());
+    moduleOutDir = new File(options.getOutDir(), module.getDeployTo());
     Util.recursiveDelete(moduleOutDir, true);
-    moduleAuxDir = new File(options.getOutDir(), module.getName() + "-aux");
-    Util.recursiveDelete(moduleAuxDir, false);
+    if (options.getExtraDir() != null) {
+      moduleExtraDir = new File(options.getExtraDir(), module.getDeployTo());
+      Util.recursiveDelete(moduleExtraDir, false);
+    }
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/Precompile.java b/dev/core/src/com/google/gwt/dev/Precompile.java
index de667a7..627ef1e 100644
--- a/dev/core/src/com/google/gwt/dev/Precompile.java
+++ b/dev/core/src/com/google/gwt/dev/Precompile.java
@@ -43,6 +43,8 @@
 import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
 import com.google.gwt.dev.util.arg.ArgHandlerScriptStyle;
 import com.google.gwt.dev.util.arg.ArgHandlerValidateOnlyFlag;
+import com.google.gwt.dev.util.arg.OptionGenDir;
+import com.google.gwt.dev.util.arg.OptionValidateOnly;
 
 import java.io.File;
 import java.util.HashSet;
@@ -57,8 +59,15 @@
  */
 public class Precompile {
 
-  static class ArgProcessor extends Link.ArgProcessor {
-    public ArgProcessor(CompilerOptions options) {
+  /**
+   * The set of options for the precompiler.
+   */
+  public interface PrecompileOptions extends JJSOptions, CompileTaskOptions,
+      OptionGenDir, OptionValidateOnly {
+  }
+
+  static class ArgProcessor extends CompileArgProcessor {
+    public ArgProcessor(PrecompileOptions options) {
       super(options);
       registerHandler(new ArgHandlerGenDir(options));
       registerHandler(new ArgHandlerScriptStyle(options));
@@ -72,32 +81,27 @@
       return Precompile.class.getName();
     }
   }
-  /**
-   * Concrete class to implement all compiler options.
-   */
-  static class CompilerOptionsImpl extends CompileTaskOptionsImpl implements
-      CompilerOptions {
 
+  static class PrecompileOptionsImpl extends CompileTaskOptionsImpl implements
+      PrecompileOptions {
     private File genDir;
     private final JJSOptionsImpl jjsOptions = new JJSOptionsImpl();
     private boolean validateOnly;
 
-    public CompilerOptionsImpl() {
+    public PrecompileOptionsImpl() {
     }
 
-    public CompilerOptionsImpl(CompilerOptions other) {
+    public PrecompileOptionsImpl(PrecompileOptions other) {
       copyFrom(other);
     }
 
-    public void copyFrom(CompilerOptions other) {
+    public void copyFrom(PrecompileOptions other) {
       super.copyFrom(other);
 
+      jjsOptions.copyFrom(other);
+
       setGenDir(other.getGenDir());
       setValidateOnly(other.isValidateOnly());
-
-      setAggressivelyOptimize(other.isAggressivelyOptimize());
-      setEnableAssertions(other.isEnableAssertions());
-      setOutput(other.getOutput());
     }
 
     public File getGenDir() {
@@ -218,7 +222,7 @@
      * shutdown AWT related threads, since the contract for their termination is
      * still implementation-dependent.
      */
-    final CompilerOptions options = new CompilerOptionsImpl();
+    final PrecompileOptions options = new PrecompileOptionsImpl();
     if (new ArgProcessor(options).processArgs(args)) {
       CompileTask task = new CompileTask() {
         public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -331,14 +335,12 @@
     }
   }
 
-  private File generatorResourcesDir;
-
   private ModuleDef module;
 
-  private final CompilerOptionsImpl options;
+  private final PrecompileOptionsImpl options;
 
-  public Precompile(CompilerOptions options) {
-    this.options = new CompilerOptionsImpl(options);
+  public Precompile(PrecompileOptions options) {
+    this.options = new PrecompileOptionsImpl(options);
   }
 
   public boolean run(TreeLogger logger) throws UnableToCompleteException {
@@ -347,7 +349,7 @@
       TreeLogger branch = logger.branch(TreeLogger.INFO,
           "Validating compilation " + module.getName());
       if (validate(branch, options, module, options.getGenDir(),
-          generatorResourcesDir)) {
+          options.getCompilerWorkDir())) {
         branch.log(TreeLogger.INFO, "Validation succeeded");
         return true;
       } else {
@@ -359,7 +361,7 @@
       TreeLogger branch = logger.branch(TreeLogger.INFO, "Precompiling module "
           + module.getName());
       Precompilation precompilation = precompile(branch, options, module,
-          options.getGenDir(), generatorResourcesDir);
+          options.getGenDir(), options.getCompilerWorkDir());
       if (precompilation != null) {
         Util.writeObjectAsFile(branch, new File(options.getCompilerWorkDir(),
             PRECOMPILATION_FILENAME), precompilation);
@@ -385,10 +387,6 @@
     this.module = ModuleDefLoader.loadFromClassPath(logger,
         options.getModuleName());
 
-    // Place generated resources inside the work dir.
-    generatorResourcesDir = new File(compilerWorkDir, "generated");
-    generatorResourcesDir.mkdirs();
-
     // TODO: All JDT checks now before even building TypeOracle?
     module.getCompilationState().compile(logger);
   }
diff --git a/dev/core/src/com/google/gwt/dev/ShellOptions.java b/dev/core/src/com/google/gwt/dev/ShellOptions.java
new file mode 100644
index 0000000..71ea4e8
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/ShellOptions.java
@@ -0,0 +1,29 @@
+/*
+ * 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.dev.jjs.JJSOptions;
+import com.google.gwt.dev.util.arg.OptionExtraDir;
+import com.google.gwt.dev.util.arg.OptionGenDir;
+import com.google.gwt.dev.util.arg.OptionLogLevel;
+import com.google.gwt.dev.util.arg.OptionOutDir;
+
+/**
+ * The complete set of options for the GWT compiler.
+ */
+public interface ShellOptions extends JJSOptions, OptionLogLevel, OptionOutDir,
+    OptionGenDir, OptionExtraDir {
+}
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
index 3d6efb6..b4e4b1e 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
@@ -87,6 +87,8 @@
 
   private CompilationState compilationState;
 
+  private String deployTo;
+
   private final List<String> entryPointTypeNames = new ArrayList<String>();
 
   private final Set<File> gwtXmlFiles = new HashSet<File>();
@@ -264,6 +266,17 @@
     return compilationState;
   }
 
+  /**
+   * Returns the desired deployment path within the output directory. The
+   * returned value will start and end with a <code>'/'</code> character.
+   */
+  public String getDeployTo() {
+    String result = (deployTo == null) ? ('/' + getName() + '/') : deployTo;
+    assert result.startsWith("/");
+    assert result.endsWith("/");
+    return result;
+  }
+
   public synchronized String[] getEntryPointTypeNames() {
     final int n = entryPointTypeNames.size();
     return entryPointTypeNames.toArray(new String[n]);
@@ -368,6 +381,26 @@
   }
 
   /**
+   * Set the deployment path for this module. Setting this value to
+   * <code>null</code> or the empty string will default to the fully-qualified
+   * module name.
+   */
+  public void setDeployTo(String deployTo) {
+    if (deployTo != null && deployTo.length() == 0) {
+      deployTo = null;
+    } else {
+      assert deployTo.startsWith("/");
+      // Ensure ends with trailing slash.
+      if (!deployTo.endsWith("/")) {
+        deployTo += '/';
+      }
+      assert deployTo.endsWith("/");
+    }
+
+    this.deployTo = deployTo;
+  }
+
+  /**
    * Override the module's apparent name. Setting this value to
    * <code>null<code> will disable the name override.
    */
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
index ebde69d..5f1287b 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
@@ -921,6 +921,7 @@
   }
 
   protected final String __module_1_rename_to = "";
+  protected final String __module_2_deploy_to = "";
 
   private final PropertyAttrCvt bindingPropAttrCvt = new PropertyAttrCvt(
       BindingProperty.class);
@@ -967,11 +968,21 @@
     registerAttributeConverter(Class.class, classAttrCvt);
   }
 
-  protected Schema __module_begin(NullableName renameTo) {
+  protected Schema __module_begin(NullableName renameTo, String deployTo)
+      throws UnableToCompleteException {
+
+    if (deployTo != null && deployTo.length() > 0) {
+      // Only absolute paths, although it is okay to have multiple slashes.
+      if (!deployTo.startsWith("/")) {
+        logger.log(TreeLogger.ERROR, "deploy-to '" + deployTo
+            + "' must begin with forward slash (e.g. '/foo')");
+        throw new UnableToCompleteException();
+      }
+    }
     return bodySchema;
   }
 
-  protected void __module_end(NullableName renameTo) {
+  protected void __module_end(NullableName renameTo, String deployTo) {
     // Maybe infer source and public.
     //
     if (!foundExplicitSourceOrSuperSource) {
@@ -986,6 +997,7 @@
 
     // We do this in __module_end so this value is never inherited
     moduleDef.setNameOverride(renameTo.token);
+    moduleDef.setDeployTo(deployTo);
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/shell/ArtifactAcceptor.java b/dev/core/src/com/google/gwt/dev/shell/ArtifactAcceptor.java
new file mode 100644
index 0000000..dfff75b
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/ArtifactAcceptor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.shell;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+
+/**
+ * A callback interface to be notified when new resources are generated.
+ * 
+ */
+public interface ArtifactAcceptor {
+  /**
+   * Called whenever new artifacts are generated.
+   */
+  void accept(TreeLogger logger, ArtifactSet newlyGeneratedArtifacts)
+      throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java b/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java
index 670af09..27ada88 100644
--- a/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java
+++ b/dev/core/src/com/google/gwt/dev/shell/GWTShellServlet.java
@@ -19,7 +19,6 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.impl.HostedModeLinker;
 import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
-import com.google.gwt.dev.GWTShell;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.jjs.JJSOptionsImpl;
@@ -117,7 +116,7 @@
 
   private int nextRequestId;
 
-  private File outDir;
+  private WorkDirs workDirs;
 
   private final Object requestIdLock = new Object();
 
@@ -426,9 +425,8 @@
       }
 
       if (foundResource == null) {
-        // Look for generated files
-        File shellDir = new File(getOutputDir(), GWTShell.GWT_SHELL_PATH
-            + File.separator + moduleName);
+        // Look for public generated files
+        File shellDir = getShellWorkDirs().getShellPublicGenDir(moduleDef);
         File requestedFile = new File(shellDir, partialPath);
         if (requestedFile.exists()) {
           try {
@@ -441,11 +439,10 @@
 
       /*
        * If the user is coming from compiled web-mode, check the linker output
-       * directory for the real bootstrap file. We'll default to using the
-       * output directory of the first linker defined in the <set-linker> tab.
+       * directory for the real bootstrap file.
        */
       if (foundResource == null) {
-        File moduleDir = new File(getOutputDir(), moduleName);
+        File moduleDir = getShellWorkDirs().getCompilerOutputDir(moduleDef);
         File requestedFile = new File(moduleDir, partialPath);
         if (requestedFile.exists()) {
           try {
@@ -617,21 +614,21 @@
     }
   }
 
-  private synchronized File getOutputDir() {
-    if (outDir == null) {
-      ServletContext servletContext = getServletContext();
-      final String attr = "com.google.gwt.dev.shell.outdir";
-      outDir = (File) servletContext.getAttribute(attr);
-      assert (outDir != null);
-    }
-    return outDir;
-  }
-
   @SuppressWarnings("unchecked")
   private Map<String, String[]> getParameterMap(HttpServletRequest request) {
     return request.getParameterMap();
   }
 
+  private synchronized WorkDirs getShellWorkDirs() {
+    if (workDirs == null) {
+      ServletContext servletContext = getServletContext();
+      final String attr = "com.google.gwt.dev.shell.workdirs";
+      workDirs = (WorkDirs) servletContext.getAttribute(attr);
+      assert (workDirs != null);
+    }
+    return workDirs;
+  }
+
   private String guessMimeType(String fullPath) {
     int dot = fullPath.lastIndexOf('.');
     if (dot != -1) {
@@ -951,7 +948,7 @@
         // ServeletContext.getResourceAsStream()
         //
         ServletContext context = new HostedModeServletContextProxy(
-            getServletContext(), moduleDef, getOutputDir());
+            getServletContext(), moduleDef, getShellWorkDirs());
         ServletConfig config = new HostedModeServletConfigProxy(
             getServletConfig(), context);
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/GWTShellServletFilter.java b/dev/core/src/com/google/gwt/dev/shell/GWTShellServletFilter.java
new file mode 100644
index 0000000..4b5c6a6
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/GWTShellServletFilter.java
@@ -0,0 +1,155 @@
+/*
+ * 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.shell;
+
+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.ShellOptions;
+import com.google.gwt.dev.cfg.ModuleDef;
+
+import org.apache.commons.collections.map.AbstractReferenceMap;
+import org.apache.commons.collections.map.ReferenceIdentityMap;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Built-in servlet for convenient access to the public path of a specified
+ * module.
+ */
+public class GWTShellServletFilter implements Filter {
+
+  private final Map<String, ModuleDef> autogenScripts = new HashMap<String, ModuleDef>();
+  /**
+   * Maintains a persistent map of linker contexts for each module, for
+   * incremental re-link with new generated artifacts.
+   */
+  @SuppressWarnings("unchecked")
+  private final Map<ModuleDef, StandardLinkerContext> linkerContextsByModule = new ReferenceIdentityMap(
+      AbstractReferenceMap.WEAK, AbstractReferenceMap.HARD, true);
+
+  private TreeLogger logger;
+
+  private final ShellOptions options;
+
+  public GWTShellServletFilter(TreeLogger logger, ShellOptions options,
+      ModuleDef[] moduleDefs) {
+    this.logger = logger;
+    this.options = options;
+    for (ModuleDef moduleDef : moduleDefs) {
+      String scriptName = moduleDef.getDeployTo() + moduleDef.getName()
+          + ".nocache.js";
+      autogenScripts.put(scriptName, moduleDef);
+    }
+  }
+
+  public void destroy() {
+  }
+
+  public void doFilter(ServletRequest req, ServletResponse resp,
+      FilterChain chain) throws IOException, ServletException {
+
+    if (req instanceof HttpServletRequest) {
+      HttpServletRequest request = (HttpServletRequest) req;
+      String pathInfo = request.getRequestURI();
+      logger.log(TreeLogger.TRACE, "Request for: " + pathInfo);
+      ModuleDef moduleDef = autogenScripts.get(pathInfo);
+      if (moduleDef != null) {
+        /*
+         * If the '?compiled' request property is specified, don't
+         * auto-generate.
+         * 
+         * TODO(scottb): does this even do anything anymore?
+         * 
+         * TODO(scottb): how do we avoid clobbering a compiled selection script?
+         */
+        if (req.getParameter("compiled") == null) {
+          try {
+            // Run the linkers for hosted mode.
+            hostedModeLink(logger.branch(TreeLogger.TRACE, "Request for '"
+                + pathInfo + "' maps to script generator for module '"
+                + moduleDef.getName() + "'"), moduleDef);
+          } catch (UnableToCompleteException e) {
+            /*
+             * The error will have already been logged. Continue, since this
+             * could actually be a request for a static file that happens to
+             * have an unfortunately confusing name.
+             */
+          }
+        }
+      }
+    }
+
+    // Do normal handling, knowing that the linkers may have run earlier to
+    // produce files we are just about to serve.
+    chain.doFilter(req, resp);
+  }
+
+  public void init(FilterConfig filterConfig) throws ServletException {
+  }
+
+  /**
+   * Called when new generated artifacts are produced.
+   */
+  public void relink(TreeLogger logger, ModuleDef moduleDef,
+      ArtifactSet newArtifacts) throws UnableToCompleteException {
+    StandardLinkerContext context = linkerContextsByModule.get(moduleDef);
+    assert context != null;
+
+    ArtifactSet artifacts = context.invokeRelink(logger, newArtifacts);
+    dumpArtifacts(logger, moduleDef, context, artifacts);
+  }
+
+  private void dumpArtifacts(TreeLogger logger, ModuleDef moduleDef,
+      StandardLinkerContext context, ArtifactSet artifacts)
+      throws UnableToCompleteException {
+    File outputPath = new File(options.getOutDir(), moduleDef.getDeployTo());
+    File extraPath = null;
+    if (options.getExtraDir() != null) {
+      extraPath = new File(options.getExtraDir(), moduleDef.getDeployTo());
+    }
+    context.produceOutputDirectory(logger, artifacts, outputPath, extraPath);
+  }
+
+  private void hostedModeLink(TreeLogger logger, ModuleDef moduleDef)
+      throws UnableToCompleteException {
+    String moduleName = moduleDef.getName();
+    logger.log(TreeLogger.TRACE, "Running linkers for module " + moduleName);
+
+    // TODO: blow away artifacts from a previous link.
+
+    // Perform the initial link.
+    StandardLinkerContext context = new StandardLinkerContext(logger,
+        moduleDef, options);
+    ArtifactSet artifacts = context.invokeLink(logger);
+    dumpArtifacts(logger, moduleDef, context, artifacts);
+
+    // Save off a new active link state (which may overwrite an old one).
+    linkerContextsByModule.put(moduleDef, context);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/HostedModeServletConfigProxy.java b/dev/core/src/com/google/gwt/dev/shell/HostedModeServletConfigProxy.java
index 147ce16..f5b0378 100644
--- a/dev/core/src/com/google/gwt/dev/shell/HostedModeServletConfigProxy.java
+++ b/dev/core/src/com/google/gwt/dev/shell/HostedModeServletConfigProxy.java
@@ -47,6 +47,7 @@
    * @return
    * @see javax.servlet.ServletConfig#getInitParameterNames()
    */
+  @SuppressWarnings("unchecked")
   public Enumeration<String> getInitParameterNames() {
     return config.getInitParameterNames();
   }
diff --git a/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java b/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java
index 5270bf1..b68f90a 100644
--- a/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java
+++ b/dev/core/src/com/google/gwt/dev/shell/HostedModeServletContextProxy.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.dev.shell;
 
-import com.google.gwt.dev.GWTShell;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.resource.Resource;
 
@@ -43,13 +42,13 @@
    * Avoid pinning my moduleDef.
    */
   private final WeakReference<ModuleDef> moduleDefRef;
-  private final File outDir;
+  private final WorkDirs workDirs;
 
   HostedModeServletContextProxy(ServletContext context, ModuleDef moduleDef,
-      File outDir) {
+      WorkDirs workDirs) {
     this.context = context;
     this.moduleDefRef = new WeakReference<ModuleDef>(moduleDef);
-    this.outDir = outDir;
+    this.workDirs = workDirs;
   }
 
   /**
@@ -65,6 +64,7 @@
    * @return
    * @see javax.servlet.ServletContext#getAttributeNames()
    */
+  @SuppressWarnings("unchecked")
   public Enumeration<String> getAttributeNames() {
     return context.getAttributeNames();
   }
@@ -78,6 +78,10 @@
     return context.getContext(arg0);
   }
 
+  public String getContextPath() {
+    return context.getContextPath();
+  }
+
   /**
    * @param arg0
    * @return
@@ -178,9 +182,8 @@
       return publicResource.getURL();
     }
 
-    // Otherwise try the path but rooted in the shell's output directory
-    File shellDir = new File(outDir, GWTShell.GWT_SHELL_PATH + File.separator
-        + moduleDef.getName());
+    // Otherwise try the path in the shell's public generated directory
+    File shellDir = workDirs.getShellPublicGenDir(moduleDef);
     File requestedFile = new File(shellDir, partialPath);
     if (requestedFile.exists()) {
       return requestedFile.toURI().toURL();
@@ -191,7 +194,8 @@
      * directory for the file. We'll default to using the output directory of
      * the first linker defined in the <set-linker> tab.
      */
-    requestedFile = new File(new File(outDir, moduleDef.getName()), partialPath);
+    File linkDir = workDirs.getCompilerOutputDir(moduleDef);
+    requestedFile = new File(linkDir, partialPath);
     if (requestedFile.exists()) {
       try {
         return requestedFile.toURI().toURL();
@@ -230,6 +234,7 @@
    * @return
    * @see javax.servlet.ServletContext#getResourcePaths(java.lang.String)
    */
+  @SuppressWarnings("unchecked")
   public Set<String> getResourcePaths(String path) {
     return context.getResourcePaths(path);
   }
@@ -268,6 +273,7 @@
    * @see javax.servlet.ServletContext#getServletNames()
    */
   @Deprecated
+  @SuppressWarnings("unchecked")
   public Enumeration<String> getServletNames() {
     return context.getServletNames();
   }
@@ -278,6 +284,7 @@
    * @see javax.servlet.ServletContext#getServlets()
    */
   @Deprecated
+  @SuppressWarnings("unchecked")
   public Enumeration<Servlet> getServlets() {
     return context.getServlets();
   }
diff --git a/dev/core/src/com/google/gwt/dev/shell/ServletContainer.java b/dev/core/src/com/google/gwt/dev/shell/ServletContainer.java
new file mode 100644
index 0000000..732998a
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/ServletContainer.java
@@ -0,0 +1,53 @@
+/*
+ * 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.shell;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+/**
+ * An instance of a servlet container that can be used by the shell. It is
+ * assumed that this servlet container serves a web app from the root directory
+ * specified by a call to
+ * {@link ServletContainerLauncher#setAppRootDir(java.io.File)}.
+ */
+public interface ServletContainer {
+
+  /**
+   * Provides the port on which the server is actually running, which can be
+   * useful when automatic port selection was requested.
+   */
+  int getPort();
+
+  /**
+   * Causes the web app to pick up changes made within the app root dir while
+   * running. This method cannot be called after {@link #stop()} has been
+   * called.
+   * 
+   * TODO(bruce): need to determine whether all the important servlet containers
+   * will let us do this (e.g. ensure they don't lock files we would need to
+   * update)
+   * 
+   * @throws UnableToCompleteException
+   */
+  void refresh() throws UnableToCompleteException;
+
+  /**
+   * Stops the running servlet container. It cannot be restarted after this.
+   * 
+   * @throws UnableToCompleteException
+   */
+  void stop() throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/ServletContainerLauncher.java b/dev/core/src/com/google/gwt/dev/shell/ServletContainerLauncher.java
new file mode 100644
index 0000000..e3896e2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/ServletContainerLauncher.java
@@ -0,0 +1,33 @@
+/*
+ * 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.shell;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.io.File;
+
+import javax.servlet.Filter;
+
+/**
+ * Defines the service provider interface for launching servlet containers that
+ * can be used by the shell.
+ */
+public interface ServletContainerLauncher {
+
+  ServletContainer start(TreeLogger topLogger, int port, File appRootDir,
+      Filter shellServletFilter) throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java b/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java
index 39cb165..38f106f 100644
--- a/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java
+++ b/dev/core/src/com/google/gwt/dev/shell/ShellModuleSpaceHost.java
@@ -21,7 +21,6 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.Rules;
-import com.google.gwt.dev.jdt.RebindOracle;
 
 import java.io.File;
 
@@ -35,13 +34,15 @@
 
   protected final TypeOracle typeOracle;
 
+  private final ArtifactAcceptor artifactAcceptor;
+
   private CompilingClassLoader classLoader;
 
   private final TreeLogger logger;
 
   private final ModuleDef module;
 
-  private RebindOracle rebindOracle;
+  private StandardRebindOracle rebindOracle;
 
   private final File shellDir;
 
@@ -52,15 +53,14 @@
    * @param saveJsni
    */
   public ShellModuleSpaceHost(TreeLogger logger, TypeOracle typeOracle,
-      ModuleDef module, File genDir, File shellDir) {
+      ModuleDef module, File genDir, File shellDir,
+      ArtifactAcceptor artifactAcceptor) {
     this.logger = logger;
     this.typeOracle = typeOracle;
     this.module = module;
     this.genDir = genDir;
-
-    // Combine the user's output dir with the module name to get the
-    // module-specific output dir.
     this.shellDir = shellDir;
+    this.artifactAcceptor = artifactAcceptor;
   }
 
   public CompilingClassLoader getClassLoader() {
@@ -108,10 +108,15 @@
         module.getCompilationState(), readySpace);
   }
 
-  public String rebind(TreeLogger rebindLogger, String sourceTypeName)
+  public String rebind(TreeLogger logger, String sourceTypeName)
       throws UnableToCompleteException {
     checkForModuleSpace();
-    return rebindOracle.rebind(rebindLogger, sourceTypeName);
+    return rebindOracle.rebind(logger, sourceTypeName, new ArtifactAcceptor() {
+      public void accept(TreeLogger logger, ArtifactSet newlyGeneratedArtifacts)
+          throws UnableToCompleteException {
+        artifactAcceptor.accept(logger, newlyGeneratedArtifacts);
+      }
+    });
   }
 
   private void checkForModuleSpace() {
diff --git a/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java b/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
index 332f7de..64687f5 100644
--- a/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
+++ b/dev/core/src/com/google/gwt/dev/shell/StandardGeneratorContext.java
@@ -25,7 +25,6 @@
 import com.google.gwt.core.ext.linker.GeneratedResource;
 import com.google.gwt.core.ext.linker.impl.StandardGeneratedResource;
 import com.google.gwt.core.ext.typeinfo.JClassType;
-import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.cfg.PublicOracle;
 import com.google.gwt.dev.javac.CompilationState;
@@ -175,7 +174,7 @@
     }
   }
 
-  private final ArtifactSet artifactSet;
+  private final ArtifactSet allGeneratedArtifacts;
 
   private final Set<GeneratedUnitWithFile> committedGeneratedCups = new HashSet<GeneratedUnitWithFile>();
 
@@ -185,9 +184,11 @@
 
   private final File genDir;
 
-  private final Set<String> generatedTypeNames = new HashSet<String>();
+  private final File generatorResourcesDir;
 
-  private final File outDir;
+  private ArtifactSet newlyGeneratedArtifacts = new ArtifactSet();
+
+  private final Set<String> newlyGeneratedTypeNames = new HashSet<String>();
 
   private final Map<OutputStream, PendingResource> pendingResourcesByOutputStream = new IdentityHashMap<OutputStream, PendingResource>();
 
@@ -203,13 +204,13 @@
    */
   public StandardGeneratorContext(CompilationState compilationState,
       PropertyOracle propOracle, PublicOracle publicOracle, File genDir,
-      File outDir, ArtifactSet artifactSet) {
+      File generatorResourcesDir, ArtifactSet allGeneratedArtifacts) {
     this.compilationState = compilationState;
     this.propOracle = propOracle;
     this.publicOracle = publicOracle;
     this.genDir = genDir;
-    this.outDir = outDir;
-    this.artifactSet = artifactSet;
+    this.generatorResourcesDir = generatorResourcesDir;
+    this.allGeneratedArtifacts = allGeneratedArtifacts;
   }
 
   /**
@@ -233,8 +234,8 @@
    */
   public void commitArtifact(TreeLogger logger, Artifact<?> artifact)
       throws UnableToCompleteException {
-    // The artifactSet will be null in hosted mode, since we never run Linkers
-    artifactSet.replace(artifact);
+    allGeneratedArtifacts.replace(artifact);
+    newlyGeneratedArtifacts.add(artifact);
   }
 
   public GeneratedResource commitResource(TreeLogger logger, OutputStream os)
@@ -279,9 +280,9 @@
    * uncommitted compilation units and to force committed compilation units to
    * be parsed and added to the type oracle.
    * 
-   * @return types generated during this object's lifetime
+   * @return any newly generated artifacts since the last call
    */
-  public final JClassType[] finish(TreeLogger logger)
+  public final ArtifactSet finish(TreeLogger logger)
       throws UnableToCompleteException {
 
     abortUncommittedResources(logger);
@@ -317,21 +318,16 @@
         compilationState.compile(logger);
       }
 
-      // Return the generated types.
+      // Make sure all generated types can be found in TypeOracle.
       TypeOracle typeOracle = getTypeOracle();
-      JClassType[] genTypes = new JClassType[genTypeNames.size()];
-      int next = 0;
-      for (Iterator<String> iter = genTypeNames.iterator(); iter.hasNext();) {
-        String genTypeName = iter.next();
-        try {
-          genTypes[next++] = typeOracle.getType(genTypeName);
-        } catch (NotFoundException e) {
+      for (String genTypeName : genTypeNames) {
+        if (typeOracle.findType(genTypeName) == null) {
           String msg = "Unable to find recently-generated type '" + genTypeName;
           logger.log(TreeLogger.ERROR, msg, null);
           throw new UnableToCompleteException();
         }
       }
-      return genTypes;
+      return newlyGeneratedArtifacts;
     } finally {
 
       // Remind the user if there uncommitted cups.
@@ -346,14 +342,11 @@
 
       uncommittedGeneratedCupsByPrintWriter.clear();
       committedGeneratedCups.clear();
-      generatedTypeNames.clear();
+      newlyGeneratedTypeNames.clear();
+      newlyGeneratedArtifacts = new ArtifactSet();
     }
   }
 
-  public File getOutputDir() {
-    return outDir;
-  }
-
   public final PropertyOracle getPropertyOracle() {
     return propOracle;
   }
@@ -380,7 +373,7 @@
     }
 
     // Has anybody tried to create this type during this iteration?
-    if (generatedTypeNames.contains(typeName)) {
+    if (newlyGeneratedTypeNames.contains(typeName)) {
       final String msg = "A request to create type '"
           + typeName
           + "' was received while the type itself was being created; this might be a generator or configuration bug";
@@ -398,7 +391,7 @@
     }
     GeneratedUnitWithFile gcup = new GeneratedUnitWithFile(qualifiedSourceName);
     uncommittedGeneratedCupsByPrintWriter.put(gcup.pw, gcup);
-    generatedTypeNames.add(typeName);
+    newlyGeneratedTypeNames.add(typeName);
 
     return gcup.pw;
   }
@@ -443,7 +436,7 @@
     }
 
     // See if the file is already committed.
-    SortedSet<GeneratedResource> resources = artifactSet.find(GeneratedResource.class);
+    SortedSet<GeneratedResource> resources = allGeneratedArtifacts.find(GeneratedResource.class);
     for (GeneratedResource resource : resources) {
       if (partialPath.equals(resource.getPartialPath())) {
         return null;
@@ -462,7 +455,8 @@
     }
 
     // Record that this file is pending.
-    PendingResource pendingResource = new PendingResource(outDir, partialPath);
+    PendingResource pendingResource = new PendingResource(
+        generatorResourcesDir, partialPath);
     OutputStream os = pendingResource.getOutputStream();
     pendingResourcesByOutputStream.put(os, pendingResource);
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java b/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
index 3cf0c7f..3d27016 100644
--- a/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
+++ b/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
@@ -53,14 +53,17 @@
 
     public Rebinder() {
       genCtx = new StandardGeneratorContext(compilationState, propOracle,
-          publicOracle, genDir, outDir, artifactSet);
+          publicOracle, genDir, generatorResourcesDir, allGeneratedArtifacts);
     }
 
-    public String rebind(TreeLogger logger, String typeName)
-        throws UnableToCompleteException {
+    public String rebind(TreeLogger logger, String typeName,
+        ArtifactAcceptor artifactAcceptor) throws UnableToCompleteException {
 
       String result = tryRebind(logger, typeName);
-      genCtx.finish(logger);
+      ArtifactSet newlyGeneratedArtifacts = genCtx.finish(logger);
+      if (!newlyGeneratedArtifacts.isEmpty() && artifactAcceptor != null) {
+        artifactAcceptor.accept(logger, newlyGeneratedArtifacts);
+      }
       if (result == null) {
         result = typeName;
       }
@@ -125,7 +128,7 @@
     }
   }
 
-  private final ArtifactSet artifactSet;
+  private final ArtifactSet allGeneratedArtifacts;
 
   private final Map<String, String> cache = new HashMap<String, String>();
 
@@ -133,7 +136,7 @@
 
   private final File genDir;
 
-  private final File outDir;
+  private final File generatorResourcesDir;
 
   private final PropertyOracle propOracle;
 
@@ -143,25 +146,30 @@
 
   public StandardRebindOracle(CompilationState compilationState,
       PropertyOracle propOracle, PublicOracle publicOracle, Rules rules,
-      File genDir, File moduleOutDir, ArtifactSet artifactSet) {
+      File genDir, File generatorResourcesDir, ArtifactSet allGeneratedArtifacts) {
     this.compilationState = compilationState;
     this.propOracle = propOracle;
     this.publicOracle = publicOracle;
     this.rules = rules;
     this.genDir = genDir;
-    this.outDir = moduleOutDir;
-    this.artifactSet = artifactSet;
+    this.generatorResourcesDir = generatorResourcesDir;
+    this.allGeneratedArtifacts = allGeneratedArtifacts;
   }
 
   public String rebind(TreeLogger logger, String typeName)
       throws UnableToCompleteException {
+    return rebind(logger, typeName, null);
+  }
+
+  public String rebind(TreeLogger logger, String typeName,
+      ArtifactAcceptor artifactAcceptor) throws UnableToCompleteException {
 
     String result = cache.get(typeName);
     if (result == null) {
       logger = Messages.TRACE_TOPLEVEL_REBIND.branch(logger, typeName, null);
 
       Rebinder rebinder = new Rebinder();
-      result = rebinder.rebind(logger, typeName);
+      result = rebinder.rebind(logger, typeName, artifactAcceptor);
       cache.put(typeName, result);
 
       Messages.TRACE_TOPLEVEL_REBIND_RESULT.log(logger, result, null);
diff --git a/dev/core/src/com/google/gwt/dev/shell/WorkDirs.java b/dev/core/src/com/google/gwt/dev/shell/WorkDirs.java
new file mode 100644
index 0000000..9e327b0
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/WorkDirs.java
@@ -0,0 +1,36 @@
+/*
+ * 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.shell;
+
+import com.google.gwt.dev.cfg.ModuleDef;
+
+import java.io.File;
+
+/**
+ * Provides information about work directories.
+ */
+public interface WorkDirs {
+  /**
+   * Gets the compiler output directory for a particular module.
+   */
+  File getCompilerOutputDir(ModuleDef moduleDef);
+
+  /**
+   * Gets the shell work directory for public generated files for a particular
+   * module.
+   */
+  File getShellPublicGenDir(ModuleDef moduleDef);
+}
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
new file mode 100644
index 0000000..9a6b12b
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -0,0 +1,256 @@
+/*
+ * 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.shell.jetty;
+
+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.dev.shell.ServletContainer;
+import com.google.gwt.dev.shell.ServletContainerLauncher;
+
+import org.mortbay.jetty.Handler;
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.nio.SelectChannelConnector;
+import org.mortbay.jetty.servlet.FilterHolder;
+import org.mortbay.jetty.webapp.WebAppContext;
+import org.mortbay.log.Log;
+import org.mortbay.log.Logger;
+
+import java.io.File;
+
+import javax.servlet.Filter;
+
+/**
+ * A launcher for an embedded Jetty server.
+ */
+public class JettyLauncher implements ServletContainerLauncher {
+
+  /**
+   * An adapter for the Jetty logging system to GWT's TreeLogger. This
+   * implementation class is only public to allow {@link Log} to instantiate it.
+   * 
+   * The weird static data / default construction setup is a game we play with
+   * {@link Log}'s static initializer to prevent the initial log message from
+   * going to stderr.
+   */
+  public static final class JettyTreeLogger implements Logger {
+    private static Type nextBranchLevel;
+    private static TreeLogger nextLogger;
+
+    /**
+     * Returns true if the default constructor can be called.
+     */
+    public static boolean isDefaultConstructionReady() {
+      return nextLogger != null;
+    }
+
+    /**
+     * Call to set initial state for default construction; must be called again
+     * each time before a default instantiation occurs.
+     */
+    public static void setDefaultConstruction(TreeLogger logger,
+        Type branchLevel) {
+      if (logger == null || branchLevel == null) {
+        throw new NullPointerException();
+      }
+      nextLogger = logger;
+      nextBranchLevel = branchLevel;
+    }
+
+    private final Type branchLevel;
+    private final TreeLogger logger;
+
+    public JettyTreeLogger() {
+      this(nextLogger, nextBranchLevel);
+      nextLogger = null;
+      nextBranchLevel = null;
+    }
+
+    public JettyTreeLogger(TreeLogger logger, Type branchLevel) {
+      if (logger == null || branchLevel == null) {
+        throw new NullPointerException();
+      }
+      this.branchLevel = branchLevel;
+      this.logger = logger;
+    }
+
+    public void debug(String msg, Object arg0, Object arg1) {
+      logger.log(TreeLogger.DEBUG, format(msg, arg0, arg1));
+    }
+
+    public void debug(String msg, Throwable th) {
+      logger.log(TreeLogger.DEBUG, msg, th);
+    }
+
+    public Logger getLogger(String name) {
+      return new JettyTreeLogger(logger.branch(branchLevel, name), branchLevel);
+    }
+
+    public void info(String msg, Object arg0, Object arg1) {
+      logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
+    }
+
+    public boolean isDebugEnabled() {
+      return logger.isLoggable(TreeLogger.DEBUG);
+    }
+
+    public void setDebugEnabled(boolean enabled) {
+      // ignored
+    }
+
+    public void warn(String msg, Object arg0, Object arg1) {
+      logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
+    }
+
+    public void warn(String msg, Throwable th) {
+      logger.log(TreeLogger.WARN, msg, th);
+    }
+
+    /**
+     * Copied from org.mortbay.log.StdErrLog.
+     */
+    private String format(String msg, Object arg0, Object arg1) {
+      int i0 = msg.indexOf("{}");
+      int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);
+
+      if (arg1 != null && i1 >= 0) {
+        msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
+      }
+      if (arg0 != null && i0 >= 0) {
+        msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
+      }
+      return msg;
+    }
+  }
+
+  private static class JettyServletContainer implements ServletContainer {
+
+    private final int actualPort;
+    private final File appRootDir;
+    private final TreeLogger logger;
+    private final WebAppContext wac;
+
+    public JettyServletContainer(TreeLogger logger, WebAppContext wac,
+        int actualPort, File appRootDir) {
+      this.logger = logger;
+      this.wac = wac;
+      this.actualPort = actualPort;
+      this.appRootDir = appRootDir;
+    }
+
+    public int getPort() {
+      return actualPort;
+    }
+
+    public void refresh() throws UnableToCompleteException {
+      String msg = "Reloading web app to reflect changes in "
+          + appRootDir.getAbsolutePath();
+      TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
+      try {
+        wac.stop();
+      } catch (Exception e) {
+        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
+        throw new UnableToCompleteException();
+      }
+
+      try {
+        wac.start();
+      } catch (Exception e) {
+        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
+        throw new UnableToCompleteException();
+      }
+
+      branch.log(TreeLogger.INFO, "Reload completed successfully");
+    }
+
+    public void stop() throws UnableToCompleteException {
+      TreeLogger branch = logger.branch(TreeLogger.INFO,
+          "Stopping Jetty server");
+      try {
+        wac.stop();
+      } catch (Exception e) {
+        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
+        throw new UnableToCompleteException();
+      }
+      branch.log(TreeLogger.INFO, "Stopped successfully");
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public ServletContainer start(TreeLogger logger, int port, File appRootDir,
+      Filter shellServletFilter) throws UnableToCompleteException {
+    checkStartParams(logger, port, appRootDir);
+
+    // The dance we do with Jetty's logging system.
+    System.setProperty("VERBOSE", "true");
+    JettyTreeLogger.setDefaultConstruction(logger, TreeLogger.INFO);
+    System.setProperty("org.mortbay.log.class", JettyTreeLogger.class.getName());
+    // Force initialization.
+    Log.isDebugEnabled();
+    if (JettyTreeLogger.isDefaultConstructionReady()) {
+      // The log system was already initialized and did not use our
+      // newly-constructed logger, set it explicitly now.
+      Log.setLog(new JettyTreeLogger());
+    }
+
+    Server server = new Server();
+    SelectChannelConnector connector = new SelectChannelConnector();
+    connector.setPort(port);
+    connector.setHost("127.0.0.1");
+    server.addConnector(connector);
+
+    // Create a new web app in the war directory.
+    WebAppContext wac = new WebAppContext(appRootDir.getAbsolutePath(), "/");
+
+    // Prevent file locking on windows; pick up file changes.
+    wac.getInitParams().put(
+        "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");
+
+    // Setup the shell servlet filter to generate nocache.js files (and run
+    // the hosted mode linker stack.
+    FilterHolder filterHolder = new FilterHolder();
+    filterHolder.setFilter(shellServletFilter);
+    wac.addFilter(filterHolder, "/*", Handler.ALL);
+
+    server.setHandler(wac);
+    server.setStopAtShutdown(true);
+
+    try {
+      server.start();
+      int actualPort = connector.getPort();
+      return new JettyServletContainer(logger, wac, actualPort, appRootDir);
+    } catch (Exception e) {
+      logger.log(TreeLogger.ERROR, "Unable to start embedded Jetty server", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
+    if (logger == null) {
+      throw new NullPointerException("logger cannot be null");
+    }
+
+    if (port < 0 || port > 65535) {
+      throw new IllegalArgumentException(
+          "port must be either 0 (for auto) or less than 65536");
+    }
+
+    if (appRootDir == null) {
+      throw new NullPointerException("app root direcotry cannot be null");
+    }
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java b/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java
index 0ebbe02..1fc2d9b 100644
--- a/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java
+++ b/dev/core/src/com/google/gwt/dev/shell/tomcat/EmbeddedTomcatServer.java
@@ -21,6 +21,7 @@
 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.util.tools.Utility;
 
 import org.apache.catalina.Connector;
@@ -60,13 +61,13 @@
   }
 
   public static synchronized String start(TreeLogger topLogger, int port,
-      File outDir) {
+      WorkDirs workDirs) {
     if (sTomcat != null) {
       throw new IllegalStateException("Embedded Tomcat is already running");
     }
 
     try {
-      new EmbeddedTomcatServer(topLogger, port, outDir);
+      new EmbeddedTomcatServer(topLogger, port, workDirs);
       return null;
     } catch (LifecycleException e) {
       String msg = e.getMessage();
@@ -145,7 +146,7 @@
   private final TreeLogger startupBranchLogger;
 
   private EmbeddedTomcatServer(final TreeLogger topLogger, int listeningPort,
-      final File outDir) throws LifecycleException {
+      final WorkDirs workDirs) throws LifecycleException {
     if (topLogger == null) {
       throw new NullPointerException("No logger specified");
     }
@@ -222,7 +223,7 @@
         if (StandardHost.PRE_INSTALL_EVENT.equals(event.getType())) {
           StandardContext webapp = (StandardContext) event.getData();
           publishShellLoggerAttribute(logger, topLogger, webapp);
-          publishShellOutDirAttribute(logger, outDir, webapp);
+          publishShellWorkDirsAttribute(logger, workDirs, webapp);
         }
       }
     });
@@ -411,12 +412,12 @@
   }
 
   /**
-   * Publish the shell's output dir as an attribute. This attribute is used to
+   * 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 publishShellOutDirAttribute(TreeLogger logger,
-      File outDirToPublish, StandardContext webapp) {
-    final String attr = "com.google.gwt.dev.shell.outdir";
-    publishAttributeToWebApp(logger, webapp, attr, outDirToPublish);
+  private void publishShellWorkDirsAttribute(TreeLogger logger,
+      WorkDirs workDirs, StandardContext webapp) {
+    final String attr = "com.google.gwt.dev.shell.workdirs";
+    publishAttributeToWebApp(logger, webapp, attr, workDirs);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerExtraDir.java b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerExtraDir.java
new file mode 100644
index 0000000..42626cf
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerExtraDir.java
@@ -0,0 +1,45 @@
+/*
+ * 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.util.arg;
+
+import com.google.gwt.util.tools.ArgHandlerDir;
+
+import java.io.File;
+
+/**
+ * Argument handler for processing the extra directory option.
+ */
+public final class ArgHandlerExtraDir extends ArgHandlerDir {
+
+  private final OptionExtraDir option;
+
+  public ArgHandlerExtraDir(OptionExtraDir option) {
+    this.option = option;
+  }
+
+  public String getPurpose() {
+    return "The directory into which extra, non-deployed files will be written";
+  }
+
+  public String getTag() {
+    return "-extra";
+  }
+
+  @Override
+  public void setDir(File dir) {
+    option.setExtraDir(dir);
+  }
+}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/OptionExtraDir.java b/dev/core/src/com/google/gwt/dev/util/arg/OptionExtraDir.java
new file mode 100644
index 0000000..586f41f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/OptionExtraDir.java
@@ -0,0 +1,34 @@
+/*
+ * 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.util.arg;
+
+import java.io.File;
+
+/**
+ * Option to set the output directory for extra artifacts.
+ */
+public interface OptionExtraDir {
+
+  /**
+   * Returns the extra resource directory.
+   */
+  File getExtraDir();
+
+  /**
+   * Sets the extra resource directory.
+   */
+  void setExtraDir(File dir);
+}
diff --git a/dev/core/test/com/google/gwt/core/ext/typeinfo/TypeOracleGenericsSupportTest.java b/dev/core/test/com/google/gwt/core/ext/typeinfo/TypeOracleGenericsSupportTest.java
deleted file mode 100644
index e69de29..0000000
--- a/dev/core/test/com/google/gwt/core/ext/typeinfo/TypeOracleGenericsSupportTest.java
+++ /dev/null
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/client/FooClient.java b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/client/FooClient.java
index e69de29..9b02688 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/client/FooClient.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/client/FooClient.java
@@ -0,0 +1,5 @@
+package com.google.gwt.dev.resource.impl.testdata.cpe1.org.example.foo.client;
+
+public class FooClient {
+  // test class
+}
\ No newline at end of file
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/server/FooServer.java b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/server/FooServer.java
index e69de29..6f2c68c 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/server/FooServer.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1/org/example/foo/server/FooServer.java
@@ -0,0 +1,5 @@
+package com.google.gwt.dev.resource.impl.testdata.cpe1.org.example.foo.server;
+
+public class FooServer {
+  // test class
+}
\ No newline at end of file
diff --git a/eclipse/dev/linux/.classpath b/eclipse/dev/linux/.classpath
index 3cc771f..4b09998 100644
--- a/eclipse/dev/linux/.classpath
+++ b/eclipse/dev/linux/.classpath
@@ -9,6 +9,7 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/ant-1.6.5.jar" sourcepath="/GWT_TOOLS/lib/apache/ant-1.6.5-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.3.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.3.1-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/ant-launcher-1.6.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/catalina-1.0.jar"/>
diff --git a/eclipse/dev/mac/.classpath b/eclipse/dev/mac/.classpath
index f9ad860..3832d9a 100644
--- a/eclipse/dev/mac/.classpath
+++ b/eclipse/dev/mac/.classpath
@@ -9,6 +9,7 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/ant-1.6.5.jar" sourcepath="/GWT_TOOLS/lib/apache/ant-1.6.5-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.3.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.3.1-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/ant-launcher-1.6.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/catalina-1.0.jar"/>
diff --git a/eclipse/dev/windows/.classpath b/eclipse/dev/windows/.classpath
index ff4799f..70fc175 100644
--- a/eclipse/dev/windows/.classpath
+++ b/eclipse/dev/windows/.classpath
@@ -9,6 +9,7 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/ant-1.6.5.jar" sourcepath="/GWT_TOOLS/lib/apache/ant-1.6.5-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.3.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.3.1-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-6.1.11.jar" sourcepath="/GWT_TOOLS/lib/jetty/jetty-6.1.11-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/ant-launcher-1.6.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/tomcat/catalina-1.0.jar"/>
diff --git a/eclipse/samples/DynaTable2/DynaTable2 compile.launch b/eclipse/samples/DynaTable2/DynaTable2 compile.launch
index 9d1d153..ff78aaf 100644
--- a/eclipse/samples/DynaTable2/DynaTable2 compile.launch
+++ b/eclipse/samples/DynaTable2/DynaTable2 compile.launch
@@ -17,7 +17,7 @@
 </listAttribute>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.GWTCompiler"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-wardir war&#13;&#10;-style PRETTY&#13;&#10;-logLevel INFO&#13;&#10;com.google.gwt.sample.dynatable.DynaTable"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-out war&#13;&#10;-style PRETTY&#13;&#10;-logLevel INFO&#13;&#10;-extra extra&#13;&#10;com.google.gwt.sample.dynatable.DynaTable2"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="DynaTable2"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea&#13;&#10;-Dgwt.devjar=C:\gwt\releases\1.6\build\staging\gwt-windows-0.0.0\gwt-dev-windows.jar"/>
 </launchConfiguration>
diff --git a/eclipse/samples/DynaTable2/DynaTable2 shell.launch b/eclipse/samples/DynaTable2/DynaTable2 hosted.launch
similarity index 91%
rename from eclipse/samples/DynaTable2/DynaTable2 shell.launch
rename to eclipse/samples/DynaTable2/DynaTable2 hosted.launch
index cca8023..e25b728 100644
--- a/eclipse/samples/DynaTable2/DynaTable2 shell.launch
+++ b/eclipse/samples/DynaTable2/DynaTable2 hosted.launch
@@ -16,8 +16,8 @@
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#13;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#13;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;DynaTable2&quot;/&gt;&#13;&#10;&lt;/runtimeClasspathEntry&gt;&#13;&#10;"/>
 </listAttribute>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.GWTShell"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-wardir war&#13;&#10;-style PRETTY&#13;&#10;-logLevel INFO&#13;&#10;DynaTable.html"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.GWTHosted"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-out war&#13;&#10;-style PRETTY&#13;&#10;-logLevel INFO&#13;&#10;-extra extra&#13;&#10;-startupUrl DynaTable2.html&#13;&#10;com.google.gwt.sample.dynatable.DynaTable2"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="DynaTable2"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea&#13;&#10;-Dgwt.devjar=C:\gwt\releases\1.6\build\staging\gwt-windows-0.0.0\gwt-dev-windows.jar"/>
 </launchConfiguration>
diff --git a/eclipse/samples/DynaTable2/DynaTable2 server.launch b/eclipse/samples/DynaTable2/DynaTable2 server.launch
index 4029b62..c2f361d 100644
--- a/eclipse/samples/DynaTable2/DynaTable2 server.launch
+++ b/eclipse/samples/DynaTable2/DynaTable2 server.launch
@@ -9,7 +9,7 @@
 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
 <listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#13;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER&quot; javaProject=&quot;DynaTable2&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#13;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#13;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/DynaTable2/jetty-6.1.11.jar&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#13;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#13;&#10;&lt;runtimeClasspathEntry containerPath=&quot;GWT_TOOLS/lib/jetty/jetty-6.1.11.jar&quot; path=&quot;3&quot; type=&quot;3&quot;/&gt;&#13;&#10;"/>
 </listAttribute>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.mortbay.jetty.Main"/>
diff --git a/eclipse/samples/DynaTable2/build.xml b/eclipse/samples/DynaTable2/build.xml
index d810bd9..c003fe4 100644
--- a/eclipse/samples/DynaTable2/build.xml
+++ b/eclipse/samples/DynaTable2/build.xml
@@ -1,11 +1,12 @@
 <project name="dynatable2" default="build" basedir=".">
   <property name="gwt.install" location="../../../build/lib" />
-  <property name="wardir" location="war" />
+  <property name="outdir" location="war" />
+  <property name="extradir" location="extra" />
 
   <target name="javac" description="Compile project to WEB-INF/classes">
-    <mkdir dir="${wardir}/WEB-INF/classes" />
+    <mkdir dir="${outdir}/WEB-INF/classes" />
     <javac srcdir="../../../samples/dynatable/src"
-        destdir="${wardir}/WEB-INF/classes"
+        destdir="${outdir}/WEB-INF/classes"
         debug="true"
         debuglevel="lines,vars,source"
         source="1.5"
@@ -20,19 +21,21 @@
   </target>
 
   <target name="deploy" description="Copy output to the war folder">
-    <mkdir dir="${wardir}/WEB-INF/lib" />
-    <copy todir="${wardir}/WEB-INF/lib" file="${gwt.install}/gwt-servlet.jar" />
+    <mkdir dir="${outdir}/WEB-INF/lib" />
+    <copy todir="${outdir}/WEB-INF/lib" file="${gwt.install}/gwt-servlet.jar" />
   </target>
 
   <target name="gwtc" depends="javac" description="Compile to JavaScript">
     <java classname="com.google.gwt.dev.GWTCompiler" fork="yes" failonerror="true">
       <jvmarg value="-Xmx256M"/>
-      <arg value="-wardir" />
-      <arg file="war" />
-      <arg value="com.google.gwt.sample.dynatable.DynaTable" />
+      <arg value="-out" />
+      <arg file="${outdir}" />
+      <arg value="-extra" />
+      <arg file="${extradir}" />
+      <arg value="com.google.gwt.sample.dynatable.DynaTable2" />
       <classpath>
         <pathelement location="../../../samples/dynatable/src" />
-        <pathelement location="${wardir}/WEB-INF/classes" />
+        <pathelement location="${outdir}/WEB-INF/classes" />
         <pathelement location="${gwt.install}/gwt-user.jar" />
         <pathelement location="${gwt.install}/gwt-dev-windows.jar" />
       </classpath>
@@ -40,11 +43,11 @@
   </target>
 
   <target name="server" depends="deploy" description="Run the deployed app in a Jetty server">
-    <echo message="PLEASE BROWSE TO: http://localhost:8888/DynaTable.html"/>
+    <echo message="PLEASE BROWSE TO: http://localhost:8888/DynaTable2.html"/>
     <java classname="org.mortbay.jetty.Main" fork="yes">
       <arg value="8888" />
       <arg value="-webapp" />
-      <arg file="war" />
+      <arg file="${outdir}" />
       <classpath>
         <pathelement location="${gwt.install}/gwt-dev-windows.jar" />
       </classpath>
@@ -55,12 +58,14 @@
     <java classname="com.google.gwt.dev.GWTShell" fork="yes" failonerror="true">
       <jvmarg value="-Xmx256M"/>
       <jvmarg value="-Dgwt.devjar=C:\gwt\releases\1.6\build\staging\gwt-windows-0.0.0\gwt-dev-windows.jar"/>
-      <arg value="-wardir" />
-      <arg file="war" />
-      <arg value="http://localhost:8888/DynaTable.html" />
+      <arg value="-out" />
+      <arg file="${outdir}" />
+      <arg value="-extra" />
+      <arg file="${extradir}" />
+      <arg value="http://localhost:8888/DynaTable2.html" />
       <classpath>
         <pathelement location="../../../samples/dynatable/src" />
-        <pathelement location="${wardir}/WEB-INF/classes" />
+        <pathelement location="${outdir}/WEB-INF/classes" />
         <pathelement location="${gwt.install}/gwt-user.jar" />
         <pathelement location="${gwt.install}/gwt-dev-windows.jar" />
       </classpath>
@@ -70,13 +75,8 @@
   <target name="build" depends="javac, gwtc, deploy" description="Build this project" />
 
   <target name="clean" description="Cleans this project's intermediate and output files">
-    <delete includeemptydirs="true" failonerror="false">
-      <fileset dir="${wardir}" includes="*" excludes="DynaTable.html"/>
-    </delete>
-    <delete dir="${wardir}/WEB-INF/classes" failonerror="false" />
-    <delete dir="${wardir}/WEB-INF/lib" failonerror="false" />
-    <delete dir="${wardir}/WEB-INF/gwt-aux" failonerror="false" />
-    <delete dir="${wardir}/WEB-INF/.gwt-tmp" failonerror="false" />
-    <delete dir="www" failonerror="false" />
+    <delete dir="${outdir}/WEB-INF/classes" failonerror="false" />
+    <delete dir="${outdir}/gwtc" failonerror="false" />
+    <delete dir="${extradir}" failonerror="false" />
   </target>
 </project>
diff --git a/eclipse/samples/DynaTable2/war/DynaTable2.css b/eclipse/samples/DynaTable2/war/DynaTable2.css
new file mode 100644
index 0000000..ab81abf
--- /dev/null
+++ b/eclipse/samples/DynaTable2/war/DynaTable2.css
@@ -0,0 +1,85 @@
+
+body {
+  background-color: white;
+  color: black;
+  font-family: Arial, sans-serif;
+  font-size: small;
+  margin: 8px;
+  margin-top: 3px;
+}
+
+.DynaTable-DynaTableWidget {
+	width: 100%;
+	border: 1px solid #ACA899;
+}
+
+
+.DynaTable-DynaTableWidget .navbar {
+	width: 100%;
+	background-color: #ECE9D8;
+	vertical-align: middle;
+	border-bottom: 1px solid #ACA899;
+}
+
+.DynaTable-DynaTableWidget .navbar button {
+	width: 3em;
+	text-align: center;
+	vertical-align: middle;
+}
+
+.DynaTable-DynaTableWidget .navbar .status {
+	vertical-align: middle;
+	padding-right: 10px;
+}
+
+.DynaTable-DynaTableWidget .table {
+	margin: 10px;
+}
+
+.DynaTable-DynaTableWidget .table td.header {
+	text-align: left;
+	font-weight: bold;
+	text-decoration: underline;
+}
+
+.DynaTable-DynaTableWidget .table td.name {
+	width: 10em;
+}
+
+.DynaTable-DynaTableWidget .table td.desc {
+	width: 20em;
+}
+
+.DynaTable-DynaTableWidget .table td.sched {
+	width: 20em;
+}
+
+.DynaTable-DynaTableWidget .table td {
+	vertical-align: top;
+}
+
+.DynaTable-DayFilterWidget {
+	margin: 3em 1em 1em 0;
+	width: 10em;
+	padding: 0px 8px 0px 8px;
+	border: 1px solid #ACA899;
+}
+
+.DynaTable-DayFilterWidget button {
+	width: 4em;
+	margin: 8px 4px 8px 4px;
+}
+
+.DynaTable-ErrorDialog {
+  border: 2px outset;
+  background-color: white;
+  width: 50%;
+}
+
+.DynaTable-ErrorDialog .Caption {
+  background-color: #C3D9FF;
+  padding: 3px;
+  margin: 2px;
+  font-weight: bold;
+  cursor: default;
+}
diff --git a/eclipse/samples/DynaTable2/war/DynaTable2.html b/eclipse/samples/DynaTable2/war/DynaTable2.html
new file mode 100644
index 0000000..8f29e24
--- /dev/null
+++ b/eclipse/samples/DynaTable2/war/DynaTable2.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--                                                                        -->
+<!-- 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+ 
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+    <link type="text/css" rel="stylesheet" href="DynaTable2.css">
+    <title></title>
+  </head>
+  <body>
+  <iframe src="javascript:''" id='__gwt_historyFrame' tabIndex='-1' style='width:0;height:0;border:0'></iframe>
+	<script type="text/javascript" language='javascript' src='gwtc/dynatable.nocache.js'></script>
+    <h1>School Schedule for Professors and Students</h1>
+    <table width="100%" border="0" summary="School Schedule for Professors and Students">
+      <tr valign="top">
+        <td id="calendar" align="center" width="90%">
+        </td>
+        <td id="days" align="center" width="10%">
+        </td>
+      </tr>
+    </table>
+  </body>
+</html>
diff --git a/eclipse/samples/DynaTable2/war/WEB-INF/web.xml b/eclipse/samples/DynaTable2/war/WEB-INF/web.xml
index 983aac0..1880c9e 100644
--- a/eclipse/samples/DynaTable2/war/WEB-INF/web.xml
+++ b/eclipse/samples/DynaTable2/war/WEB-INF/web.xml
@@ -1,16 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <web-app>
 
-  <!-- GWT REGENERATED BEGIN -->
-  <servlet>
-    <servlet-name>calendar</servlet-name>
-    <servlet-class>com.google.gwt.sample.dynatable.server.SchoolCalendarServiceImpl</servlet-class>
-  </servlet>
-  
-  <servlet-mapping>
-    <servlet-name>calendar</servlet-name>
-    <url-pattern>/calendar</url-pattern>
-  </servlet-mapping>
-  <!-- GWT REGENERATED END -->
+	<servlet>
+		<servlet-name>calendar</servlet-name>
+		<servlet-class>com.google.gwt.sample.dynatable.server.SchoolCalendarServiceImpl</servlet-class>
+	</servlet>
+	
+	<servlet-mapping>
+		<servlet-name>calendar</servlet-name>
+		<url-pattern>/gwtc/calendar</url-pattern>
+	</servlet-mapping>
 
 </web-app>
diff --git a/samples/dynatable/src/com/google/gwt/sample/dynatable/DynaTable2.gwt.xml b/samples/dynatable/src/com/google/gwt/sample/dynatable/DynaTable2.gwt.xml
new file mode 100644
index 0000000..64a55e8
--- /dev/null
+++ b/samples/dynatable/src/com/google/gwt/sample/dynatable/DynaTable2.gwt.xml
@@ -0,0 +1,20 @@
+<!--                                                                        -->
+<!-- Copyright 2007 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module rename-to="dynatable" deploy-to="/gwtc">
+	<inherits name='com.google.gwt.user.User'/>
+	<entry-point class='com.google.gwt.sample.dynatable.client.DynaTable'/>
+</module>
+
+
diff --git a/user/build.xml b/user/build.xml
index 3cb3045..0539253 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -50,9 +50,10 @@
       <classpath>
         <pathelement location="${javac.out}" />
         <pathelement location="${gwt.build}/out/dev/core/bin-test" />
+        <pathelement location="${gwt.tools.lib}/tomcat/servlet-api-2.4.jar" />
         <pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
         <pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
-        <pathelement location="${gwt.dev.staging.jar}" />
+        <pathelement location="${gwt.dev.jar}" />
       </classpath>
     </gwt.javac>
   </target>
diff --git a/user/src/com/google/gwt/i18n/client/impl/ConstantMap.java b/user/src/com/google/gwt/i18n/client/impl/ConstantMap.java
index 9a7779b..0ecec17 100644
--- a/user/src/com/google/gwt/i18n/client/impl/ConstantMap.java
+++ b/user/src/com/google/gwt/i18n/client/impl/ConstantMap.java
@@ -15,137 +15,112 @@
  */
 package com.google.gwt.i18n.client.impl;
 
-import java.util.ArrayList;
-import java.util.Collection;
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
 /**
- * Read only Map used when returning <code>Constants</code> maps. Preserves
- * order. ConstantMap should only be created or modified by GWT, as constant
- * maps are constructed using a very stereotyped algorithm, which allows
- * <code>ConstantMap</code> to maintain order with very little code. In
- * specific, no elements are every removed from them and all elements are added
- * before the first user operation.
+ * Map used when creating <code>Constants</code> maps. This class is to be
+ * used only by the GWT code. The map is immediately wrapped in
+ * Collections.unmodifiableMap(..) preventing any changes after construction.
  */
-public class ConstantMap extends HashMap<String, String> {
+public class ConstantMap extends AbstractMap<String, String> {
 
-  private static class DummyMapEntry implements Map.Entry<String, String> {
-    private final String key;
+  /**
+   * A cache of a synthesized entry set.
+   */
+  private Set<Map.Entry<String, String>> entries;
 
-    private final String value;
+  /**
+   * The original set of keys.
+   */
+  private final String[] keys;
 
-    DummyMapEntry(String key, String value) {
-      this.key = key;
-      this.value = value;
-    }
+  /*
+   * Stores a fast lookup in a JSO using ':' to prevent conflict with built-in
+   * JavaScript properties.
+   */
+  @SuppressWarnings("unused")
+  private JavaScriptObject map;
 
-    public String getKey() {
-      return key;
-    }
+  public ConstantMap(String keys[], String values[]) {
+    this.keys = keys;
 
-    public String getValue() {
-      return value;
-    }
+    init();
 
-    public String setValue(String arg0) {
-      throw new UnsupportedOperationException();
+    for (int i = 0; i < keys.length; ++i) {
+      putImpl(keys[i], values[i]);
     }
   }
 
-  private class OrderedConstantSet<T> extends ArrayList<T> implements Set<T> {
-    private class ImmutableIterator implements Iterator<T> {
-      private final Iterator<T> base;
-
-      ImmutableIterator(Iterator<T> base) {
-        this.base = base;
-      }
-
-      public boolean hasNext() {
-        return base.hasNext();
-      }
-
-      public T next() {
-        return base.next();
-      }
-
-      public void remove() {
-        throw new UnsupportedOperationException("Immutable set");
-      }
-    }
-
-    @Override
-    public void clear() {
-      throw new UnsupportedOperationException("Immutable set");
-    }
-
-    @Override
-    public Iterator<T> iterator() {
-      Iterator<T> base = super.iterator();
-      return new ImmutableIterator(base);
-    }
-  }
-
-  private OrderedConstantSet<Map.Entry<String, String>> entries;
-
-  private final OrderedConstantSet<String> keys = new OrderedConstantSet<String>();
-
-  private OrderedConstantSet<String> values;
-
   @Override
-  public void clear() {
-    throw unsupported("clear");
+  public boolean containsKey(Object key) {
+    return get(key) != null;
   }
 
   @Override
   public Set<Map.Entry<String, String>> entrySet() {
     if (entries == null) {
-      entries = new OrderedConstantSet<Map.Entry<String, String>>();
-      for (int i = 0; i < keys.size(); i++) {
-        String key = keys.get(i);
-        String value = get(key);
-        entries.add(new DummyMapEntry(key, value));
+      Map<String, String> copy = new HashMap<String, String>();
+      for (String key : keys) {
+        copy.put(key, get(key));
       }
+      entries = Collections.unmodifiableMap(copy).entrySet();
     }
     return entries;
   }
 
   @Override
+  public String get(Object key) {
+    return (key instanceof String) ? get((String) key) : null;
+  }
+
+  public native String get(String key) /*-{
+    // Prepend ':' to avoid conflicts with built-in Object properties.
+    return this.@com.google.gwt.i18n.client.impl.ConstantMap::map[':' + key];
+  }-*/;
+
+  @Override
   public Set<String> keySet() {
-    return keys;
-  }
-
-  @Override
-  public String put(String key, String value) {
-    // We may want to find a more efficient implementation later.
-    boolean exists = keys.contains(key);
-    if (!exists) {
-      keys.add(key);
-    }
-    return super.put(key, value);
-  }
-
-  @Override
-  public String remove(Object key) {
-    throw unsupported("remove");
-  }
-
-  @Override
-  public Collection<String> values() {
-    if (values == null) {
-      values = new OrderedConstantSet<String>();
-      for (int i = 0; i < keys.size(); i++) {
-        Object element = keys.get(i);
-        values.add(this.get(element));
+    return new AbstractSet<String>() {
+      @Override
+      public boolean contains(Object o) {
+        return containsKey(o);
       }
-    }
-    return values;
+
+      @Override
+      public Iterator<String> iterator() {
+        return Arrays.asList(keys).iterator();
+      }
+
+      @Override
+      public int size() {
+        return ConstantMap.this.size();
+      }
+    };
   }
 
-  private UnsupportedOperationException unsupported(String operation) {
-    return new UnsupportedOperationException(operation
-        + " not supported on a constant map");
+  @Override
+  public int size() {
+    return keys.length;
   }
+
+  /**
+   * Overridable for testing purposes, see ConstantMapTest.
+   */
+  protected void init() {
+    map = JavaScriptObject.createObject();
+  }
+
+  protected native void putImpl(String key, String value) /*-{
+    // Prepend ':' to avoid conflicts with built-in Object properties.
+    this.@com.google.gwt.i18n.client.impl.ConstantMap::map[':' + key] = value;
+  }-*/;
 }
diff --git a/user/src/com/google/gwt/i18n/rebind/ConstantsMapMethodCreator.java b/user/src/com/google/gwt/i18n/rebind/ConstantsMapMethodCreator.java
index e18625c..a192966 100644
--- a/user/src/com/google/gwt/i18n/rebind/ConstantsMapMethodCreator.java
+++ b/user/src/com/google/gwt/i18n/rebind/ConstantsMapMethodCreator.java
@@ -22,10 +22,16 @@
 import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
 import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
 
+import java.util.SortedMap;
+import java.util.TreeMap;
+
 /**
  * Creator for methods of the form Map getX() .
  */
 class ConstantsMapMethodCreator extends AbstractLocalizableMethodCreator {
+
+  static final String GENERIC_STRING_MAP_TYPE = "java.util.Map<java.lang.String, java.lang.String>";
+
   /**
    * Constructor for localizable returnType method creator.
    * 
@@ -40,13 +46,13 @@
    * 
    * @param logger TreeLogger instance for logging
    * @param method method body to create
-   * @param key value to create map from
+   * @param mapName name to create map from
    * @param resourceList AbstractResource for key lookup
    * @param locale locale to use for localized string lookup
    */
   @Override
-  public void createMethodFor(TreeLogger logger, JMethod method, String key,
-      ResourceList resourceList, String locale) {
+  public void createMethodFor(TreeLogger logger, JMethod method,
+      String mapName, ResourceList resourceList, String locale) {
     String methodName = method.getName();
     if (method.getParameters().length > 0) {
       error(
@@ -59,31 +65,53 @@
     enableCache();
     // check cache for array
     String constantMapClassName = ConstantMap.class.getCanonicalName();
-    println(constantMapClassName + " args = (" + constantMapClassName + ") cache.get("
-        + wrap(methodName) + ");");
+    println(GENERIC_STRING_MAP_TYPE + " args = (" + GENERIC_STRING_MAP_TYPE
+        + ") cache.get(" + wrap(methodName) + ");");
     // if not found create Map
     println("if (args == null) {");
     indent();
-    println("args = new " + constantMapClassName + "();");
-    String value;
+    println("args = new " + constantMapClassName + "(new String[] {");
+    String keyString;
     try {
-      value = resourceList.getRequiredStringExt(key, null);
+      keyString = resourceList.getRequiredStringExt(mapName, null);
     } catch (MissingResourceException e) {
       e.setDuring("getting key list");
       throw e;
     }
-    String[] args = ConstantsStringArrayMethodCreator.split(value);
 
-    for (int i = 0; i < args.length; i++) {
+    String[] keys = ConstantsStringArrayMethodCreator.split(keyString);
+    ResourceList resources = getResources();
+    SortedMap<String, String> map = new TreeMap<String, String>();
+    for (String key : keys) {
+      if (key.length() == 0) {
+        continue;
+      }
+
       try {
-        key = args[i];
-        String keyValue = getResources().getString(key);
-        println("args.put(" + wrap(key) + ", " + wrap(keyValue) + ");");
+        String value = resources.getRequiredString(key);
+        map.put(key, value);
       } catch (MissingResourceException e) {
         e.setDuring("implementing map");
         throw e;
       }
     }
+
+    indent();
+    indent();
+    for (String key : map.keySet()) {
+      println(wrap(key) + ", ");
+    }
+    outdent();
+    println("},");
+    indent();
+    println("new String[] {");
+    for (String key : map.keySet()) {
+      String value = map.get(key);
+      println(wrap(value) + ",");
+    }
+    outdent();
+    println("});");
+    outdent();
     println("cache.put(" + wrap(methodName) + ", args);");
     outdent();
     println("};");
diff --git a/user/src/com/google/gwt/i18n/rebind/ConstantsWithLookupImplCreator.java b/user/src/com/google/gwt/i18n/rebind/ConstantsWithLookupImplCreator.java
index 433f698..8a7272b 100644
--- a/user/src/com/google/gwt/i18n/rebind/ConstantsWithLookupImplCreator.java
+++ b/user/src/com/google/gwt/i18n/rebind/ConstantsWithLookupImplCreator.java
@@ -23,7 +23,6 @@
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.core.ext.typeinfo.TypeOracleException;
-import com.google.gwt.i18n.client.impl.ConstantMap;
 import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
 import com.google.gwt.user.rebind.AbstractMethodCreator;
 import com.google.gwt.user.rebind.SourceWriter;
@@ -125,7 +124,7 @@
       namesToMethodCreators.put("getMap", new LookupMethodCreator(this, mapType) {
         @Override
         public String getReturnTypeName() {
-          return ConstantMap.class.getCanonicalName();
+          return ConstantsMapMethodCreator.GENERIC_STRING_MAP_TYPE;
         }
       });
 
diff --git a/user/src/com/google/gwt/user/client/impl/DOMImpl.java b/user/src/com/google/gwt/user/client/impl/DOMImpl.java
index b9d1a5b..30da21c 100644
--- a/user/src/com/google/gwt/user/client/impl/DOMImpl.java
+++ b/user/src/com/google/gwt/user/client/impl/DOMImpl.java
@@ -33,16 +33,16 @@
    */
   protected static boolean isMyListener(Object object) {
     /*
-     * The first test ensures the Object belongs to this module in hosted mode,
-     * because this hosted mode class loader will have a different copy of the
-     * EventListener class than some other module would have.
+     * The first test ensures the Object belongs to this module in web mode by
+     * ensuring this is not a JavaScriptObject. In web mode, foreign Java
+     * objects appear to be JavaScriptObject. See Cast.isJavaScriptObject().
      * 
-     * However, in web mode we could still get a collision where another module
-     * happens to use the same typeId. The second test ensures the Object is not
-     * "foreign". See Cast.isJavaScriptObject().
+     * The second test then checks the exact type.
+     * 
+     * TODO: make the generated code smaller!
      */
-    return object instanceof com.google.gwt.user.client.EventListener
-        && !(object instanceof JavaScriptObject);
+    return !(object instanceof JavaScriptObject)
+        && (object instanceof com.google.gwt.user.client.EventListener);
   }
 
   public native void eventCancelBubble(Event evt, boolean cancel) /*-{
diff --git a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
index e5e9747..7e11b43 100644
--- a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
+++ b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
@@ -119,12 +119,13 @@
 
     if (serializationPolicy == null) {
       // Failed to get the requested serialization policy; use the default
-      getServletContext().log(
+      log(
           "WARNING: Failed to get the SerializationPolicy '"
               + strongName
               + "' for module '"
               + moduleBaseURL
-              + "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
+              + "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.",
+          null);
       serializationPolicy = RPC.getDefaultSerializationPolicy();
     }
 
@@ -164,7 +165,7 @@
       return RPC.invokeAndEncodeResponse(this, rpcRequest.getMethod(),
           rpcRequest.getParameters(), rpcRequest.getSerializationPolicy());
     } catch (IncompatibleRemoteServiceException ex) {
-      getServletContext().log(
+      log(
           "An IncompatibleRemoteServiceException was thrown while processing this call.",
           ex);
       return RPC.encodeResponseForFailure(null, ex);
@@ -197,7 +198,7 @@
         modulePath = new URL(moduleBaseURL).getPath();
       } catch (MalformedURLException ex) {
         // log the information, we will default
-        getServletContext().log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
+        log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
       }
     }
 
@@ -214,7 +215,7 @@
           + ", is not in the same web application as this servlet, "
           + contextPath
           + ".  Your module may not be properly configured or your client and server code maybe out of date.";
-      getServletContext().log(message);
+      log(message, null);
     } else {
       // Strip off the context path from the module base URL. It should be a
       // strict prefix.
@@ -232,19 +233,17 @@
             serializationPolicy = SerializationPolicyLoader.loadFromStream(is,
                 null);
           } catch (ParseException e) {
-            getServletContext().log(
-                "ERROR: Failed to parse the policy file '"
-                    + serializationPolicyFilePath + "'", e);
+            log("ERROR: Failed to parse the policy file '"
+                + serializationPolicyFilePath + "'", e);
           } catch (IOException e) {
-            getServletContext().log(
-                "ERROR: Could not read the policy file '"
-                    + serializationPolicyFilePath + "'", e);
+            log("ERROR: Could not read the policy file '"
+                + serializationPolicyFilePath + "'", e);
           }
         } else {
           String message = "ERROR: The serialization policy file '"
               + serializationPolicyFilePath
               + "' was not found; did you forget to include it in this deployment?";
-          getServletContext().log(message);
+          log(message, null);
         }
       } finally {
         if (is != null) {
@@ -324,7 +323,7 @@
    * Override this method in order to control the parsing of the incoming
    * request. For example, you may want to bypass the check of the Content-Type
    * and character encoding headers in the request, as some proxies re-write the
-   * request headers.  Note that bypassing these checks may expose the servlet to
+   * request headers. Note that bypassing these checks may expose the servlet to
    * some cross-site vulnerabilities.
    * 
    * @param request the incoming request
diff --git a/user/super/com/google/gwt/emul/java/util/Collections.java b/user/super/com/google/gwt/emul/java/util/Collections.java
index 50963d9..fdb3c9a 100644
--- a/user/super/com/google/gwt/emul/java/util/Collections.java
+++ b/user/super/com/google/gwt/emul/java/util/Collections.java
@@ -102,6 +102,7 @@
       throw new UnsupportedOperationException();
     }
 
+    @Override
     public boolean equals(Object o) {
       return list.equals(o);
     }
@@ -110,6 +111,7 @@
       return list.get(index);
     }
 
+    @Override
     public int hashCode() {
       return list.hashCode();
     }
@@ -118,6 +120,7 @@
       return list.indexOf(o);
     }
 
+    @Override
     public boolean isEmpty() {
       return list.isEmpty();
     }
@@ -155,6 +158,7 @@
           this.entry = entry;
         }
 
+        @Override
         public boolean equals(Object o) {
           return entry.equals(o);
         }
@@ -167,6 +171,7 @@
           return entry.getValue();
         }
 
+        @Override
         public int hashCode() {
           return entry.hashCode();
         }
@@ -175,6 +180,7 @@
           throw new UnsupportedOperationException();
         }
 
+        @Override
         public String toString() {
           return entry.toString();
         }
@@ -186,14 +192,17 @@
         super((Set<? extends Entry<K, V>>) s);
       }
 
+      @Override
       public boolean contains(Object o) {
         return coll.contains(o);
       }
 
+      @Override
       public boolean containsAll(Collection<?> o) {
         return coll.containsAll(o);
       }
 
+      @Override
       @SuppressWarnings("unchecked")
       public Iterator<Map.Entry<K, V>> iterator() {
         final Iterator<Map.Entry<K, V>> it = (Iterator<Entry<K, V>>) coll.iterator();
@@ -212,11 +221,13 @@
         };
       }
 
+      @Override
       @SuppressWarnings("unchecked")
       public Object[] toArray() {
         return toArray(super.toArray());
       }
 
+      @Override
       @SuppressWarnings("unchecked")
       public <T> T[] toArray(T[] a) {
         Object[] result = super.toArray(a);
@@ -227,7 +238,10 @@
       }
     }
 
+    private transient UnmodifiableSet<Map.Entry<K, V>> entrySet;
+    private transient UnmodifiableSet<K> keySet;
     private final Map<? extends K, ? extends V> map;
+    private transient UnmodifiableCollection<V> values;
 
     public UnmodifiableMap(Map<? extends K, ? extends V> map) {
       this.map = map;
@@ -246,9 +260,13 @@
     }
 
     public Set<Map.Entry<K, V>> entrySet() {
-      return new UnmodifiableEntrySet<K, V>(map.entrySet());
+      if (entrySet == null) {
+        entrySet = new UnmodifiableEntrySet<K, V>(map.entrySet());
+      }
+      return entrySet;
     }
 
+    @Override
     public boolean equals(Object o) {
       return map.equals(o);
     }
@@ -257,6 +275,7 @@
       return map.get(key);
     }
 
+    @Override
     public int hashCode() {
       return map.hashCode();
     }
@@ -266,7 +285,10 @@
     }
 
     public Set<K> keySet() {
-      return unmodifiableSet(map.keySet());
+      if (keySet == null) {
+        keySet = new UnmodifiableSet<K>(map.keySet());
+      }
+      return keySet;
     }
 
     public V put(K key, V value) {
@@ -285,12 +307,16 @@
       return map.size();
     }
 
+    @Override
     public String toString() {
       return map.toString();
     }
 
     public Collection<V> values() {
-      return unmodifiableCollection(map.values());
+      if (values == null) {
+        values = new UnmodifiableCollection<V>(map.values());
+      }
+      return values;
     }
   }
 
@@ -307,10 +333,12 @@
       super(set);
     }
 
+    @Override
     public boolean equals(Object o) {
       return coll.equals(o);
     }
 
+    @Override
     public int hashCode() {
       return coll.hashCode();
     }
diff --git a/user/test/com/google/gwt/i18n/ConstantMapTest.java b/user/test/com/google/gwt/i18n/ConstantMapTest.java
index 95a0b7f..8503fc7 100644
--- a/user/test/com/google/gwt/i18n/ConstantMapTest.java
+++ b/user/test/com/google/gwt/i18n/ConstantMapTest.java
@@ -17,19 +17,64 @@
 
 import com.google.gwt.i18n.client.impl.ConstantMap;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
- * TODO: document me.
+ * Test ConstantMap using Apache's tests.
  */
 public class ConstantMapTest extends MapTestBase {
 
-  protected Map makeEmptyMap() {
-    return new ConstantMap();
+  private static final class ConstantMapNoJsni extends ConstantMap {
+    private Map<String, String> noJsniImpl;
+
+    private ConstantMapNoJsni(String[] keys, String[] values) {
+      super(keys, values);
+    }
+
+    @Override
+    public String get(String key) {
+      return noJsniImpl.get(key);
+    }
+
+    @Override
+    protected void init() {
+      noJsniImpl = new HashMap<String, String>();
+    }
+
+    @Override
+    protected void putImpl(String key, String value) {
+      noJsniImpl.put(key, value);
+    }
   }
 
+  @Override
   protected boolean isRemoveModifiable() {
     return false;
   }
 
+  @Override
+  protected Map<String, String> makeEmptyMap() {
+    return Collections.unmodifiableMap(new ConstantMapNoJsni(new String[] {},
+        new String[] {}));
+  }
+
+  @Override
+  protected Map<String, String> makeFullMap() {
+    String[] keys = Arrays.asList(getSampleKeys()).toArray(new String[0]);
+    String[] values = Arrays.asList(getSampleValues()).toArray(new String[0]);
+    return Collections.unmodifiableMap(new ConstantMapNoJsni(keys, values));
+  }
+
+  @Override
+  protected boolean useNullKey() {
+    return false;
+  }
+
+  @Override
+  protected boolean useNullValue() {
+    return false;
+  }
 }
diff --git a/user/test/com/google/gwt/i18n/client/I18NTest.java b/user/test/com/google/gwt/i18n/client/I18NTest.java
index 5889825..394b090 100644
--- a/user/test/com/google/gwt/i18n/client/I18NTest.java
+++ b/user/test/com/google/gwt/i18n/client/I18NTest.java
@@ -20,7 +20,6 @@
 import com.google.gwt.i18n.client.gen.Colors;
 import com.google.gwt.i18n.client.gen.Shapes;
 import com.google.gwt.i18n.client.gen.TestMessages;
-import com.google.gwt.i18n.client.impl.ConstantMap;
 import com.google.gwt.i18n.client.resolutiontest.Inners;
 import com.google.gwt.i18n.client.resolutiontest.Inners.ExtendsInnerInner;
 import com.google.gwt.i18n.client.resolutiontest.Inners.HasInner;
@@ -37,6 +36,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.MissingResourceException;
@@ -189,161 +189,157 @@
     TestConstants types = (TestConstants) GWT.create(TestConstants.class);
 
     Map<String, String> map = types.mapABCD();
-    assertEquals(4, map.size());
-    assertEquals("valueA", map.get("keyA"));
-    assertEquals("valueB", map.get("keyB"));
-    assertEquals("valueC", map.get("keyC"));
-    assertEquals("valueD", map.get("keyD"));
-
+    Map<String, String> expectedMap = getMapFromArrayUsingASimpleRule(new String[] {
+        "A", "B", "C", "D"});
     assertNull(map.get("bogus"));
+    compareMapsComprehensively(map, expectedMap);
 
-    Set<String> keys = map.keySet();
-    Iterator<String> keyIter = keys.iterator();
-    assertEquals("keyA", keyIter.next());
-    assertEquals("keyB", keyIter.next());
-    assertEquals("keyC", keyIter.next());
-    assertEquals("keyD", keyIter.next());
-    assertFalse(keyIter.hasNext());
-
-    Collection<String> values = map.values();
-    Iterator<String> valueIter = values.iterator();
-    assertEquals("valueA", valueIter.next());
-    assertEquals("valueB", valueIter.next());
-    assertEquals("valueC", valueIter.next());
-    assertEquals("valueD", valueIter.next());
-    assertFalse(keyIter.hasNext());
-
+    /*
+     * Test if the returned map can be modified in any way. Things are working
+     * as expected if exceptions are thrown in each case.
+     */
+    String failureMessage = "Should have thrown UnsupportedOperationException";
+    /* test map operations */
     try {
       map.remove("keyA");
-      fail("Should have thrown UnsupportedOperationException");
+      fail(failureMessage + " on map.remove");
     } catch (UnsupportedOperationException e) {
-      // good if an exception was caught
     }
-
     try {
-      keys.clear();
-      fail("Should have thrown UnsupportedOperationException");
+      map.put("keyA", "allA");
+      fail(failureMessage + "on map.put of existing key");
     } catch (UnsupportedOperationException e) {
-      // good if an exception was caught
+    }
+    try {
+      map.put("keyZ", "allZ");
+      fail(failureMessage + "on map.put of new key");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.clear();
+      fail(failureMessage + " on map.clear");
+    } catch (UnsupportedOperationException e) {
     }
 
-    // TODO: fixme -- values are supposed to be backed by the map and should
-    // fail if modified
-    // try {
-    // Iterator nonmutableIter = keys.iterator();
-    // nonmutableIter.next();
-    // nonmutableIter.remove();
-    // fail("Should have thrown UnsupportedOperationException");
-    // } catch (UnsupportedOperationException e) {
-    // // good if an exception was caught
-    // }
+    /* test map.keySet() operations */
+    try {
+      map.keySet().add("keyZ");
+      fail(failureMessage + " on map.keySet().add");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.keySet().remove("keyA");
+      fail(failureMessage + " on map.keySet().remove");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.keySet().clear();
+      fail(failureMessage + " on map.keySet().clear");
+    } catch (UnsupportedOperationException e) {
+    }
+
+    /* test map.values() operations */
+    try {
+      map.values().add("valueZ");
+      fail(failureMessage + " on map.values().add");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.values().remove("valueA");
+      fail(failureMessage + " on map.values().clear()");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.values().clear();
+      fail(failureMessage + " on map.values().clear()");
+    } catch (UnsupportedOperationException e) {
+    }
+
+    /* test map.entrySet() operations */
+    Map.Entry<String, String> firstEntry = map.entrySet().iterator().next();
+    try {
+      map.entrySet().clear();
+      fail(failureMessage + "on map.entrySet().clear");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.entrySet().remove(firstEntry);
+      fail(failureMessage + " on map.entrySet().remove");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.entrySet().add(firstEntry);
+      fail(failureMessage + "on map.entrySet().add");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      firstEntry.setValue("allZ");
+      fail(failureMessage + "on firstEntry.setValue");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      map.clear();
+      fail(failureMessage + " on map.clear");
+    } catch (UnsupportedOperationException e) {
+    }
   }
 
   /**
-   * Tests focus on just the key order, since ABCD exercises the map.
+   * Tests exercise the cache.
    */
   public void testConstantMapBACD() {
     TestConstants types = (TestConstants) GWT.create(TestConstants.class);
-
-    ConstantMap map = (ConstantMap) types.mapBACD();
-
-    Set<String> keys = map.keySet();
-    Iterator<String> keyIter = keys.iterator();
-    assertEquals("keyB", keyIter.next());
-    assertEquals("keyA", keyIter.next());
-    assertEquals("keyC", keyIter.next());
-    assertEquals("keyD", keyIter.next());
-
-    Collection<String> values = map.values();
-    Iterator<String> valueIter = values.iterator();
-    assertEquals("valueB", valueIter.next());
-    assertEquals("valueA", valueIter.next());
-    assertEquals("valueC", valueIter.next());
-    assertEquals("valueD", valueIter.next());
+    Map<String, String> map = types.mapBACD();
+    Map<String, String> expectedMap = getMapFromArrayUsingASimpleRule(new String[] {
+        "B", "A", "C", "D"});
+    compareMapsComprehensively(map, expectedMap);
   }
 
   /**
-   * Tests focus on correctness of entries, since ABCD exercises the map.
+   * Tests exercise the cache.
    */
   public void testConstantMapBBB() {
     TestConstants types = (TestConstants) GWT.create(TestConstants.class);
-
-    ConstantMap map = (ConstantMap) types.mapBBB();
-
-    assertEquals(1, map.size());
-
-    Set<String> keys = map.keySet();
-    assertEquals(1, keys.size());
-    Iterator<String> keyIter = keys.iterator();
-    assertEquals("keyB", keyIter.next());
-
-    Collection<String> values = map.values();
-    assertEquals(1, values.size());
-    Iterator<String> valueIter = values.iterator();
-    assertEquals("valueB", valueIter.next());
+    Map<String, String> map = types.mapBBB();
+    Map<String, String> expectedMap = getMapFromArrayUsingASimpleRule(new String[] {"B"});
+    compareMapsComprehensively(map, expectedMap);
   }
 
   /**
-   * Tests focus on just the key order, since ABCD exercises the map.
+   * Tests exercise the cache and check if Map works as the declared return
+   * type.
    */
-  public void testConstantMapDBCA() {
+  @SuppressWarnings("unchecked")
+  public void testConstantMapDCBA() {
     TestConstants types = (TestConstants) GWT.create(TestConstants.class);
-
-    ConstantMap map = (ConstantMap) types.mapDCBA();
-
-    Set<String> keys = map.keySet();
-    Iterator<String> keyIter = keys.iterator();
-    assertEquals("keyD", keyIter.next());
-    assertEquals("keyC", keyIter.next());
-    assertEquals("keyB", keyIter.next());
-    assertEquals("keyA", keyIter.next());
-
-    Collection<String> values = map.values();
-    Iterator<String> valueIter = values.iterator();
-    assertEquals("valueD", valueIter.next());
-    assertEquals("valueC", valueIter.next());
-    assertEquals("valueB", valueIter.next());
-    assertEquals("valueA", valueIter.next());
+    Map<String, String> map = types.mapDCBA();
+    Map<String, String> expectedMap = getMapFromArrayUsingASimpleRule(new String[] {
+        "D", "C", "B", "A"});
+    compareMapsComprehensively(map, expectedMap);
   }
 
   /**
    * Tests focus on correctness of entries, since ABCD exercises the map.
    */
+  public void testConstantMapEmpty() {
+    TestConstants types = (TestConstants) GWT.create(TestConstants.class);
+    Map<String, String> map = types.mapEmpty();
+    Map<String, String> expectedMap = new HashMap<String, String>();
+    compareMapsComprehensively(map, expectedMap);
+  }
+
+  /**
+   * Tests exercise the cache and check if Map works as the declared return
+   * type.
+   */
   public void testConstantMapXYZ() {
     TestConstants types = (TestConstants) GWT.create(TestConstants.class);
-
-    ConstantMap map = (ConstantMap) types.mapXYZ();
-
-    assertEquals(3, map.size());
-
-    Set<String> keys = map.keySet();
-    assertEquals(3, keys.size());
-    Iterator<String> keyIter = keys.iterator();
-    assertEquals("keyX", keyIter.next());
-    assertEquals("keyY", keyIter.next());
-    assertEquals("keyZ", keyIter.next());
-
-    Collection<String> values = map.values();
-    assertEquals(3, values.size());
-    Iterator<String> valueIter = values.iterator();
-    assertEquals("valueZ", valueIter.next());
-    assertEquals("valueZ", valueIter.next());
-    assertEquals("valueZ", valueIter.next());
-
-    Set<Map.Entry<String, String>> entries = map.entrySet();
-    assertEquals(3, entries.size());
-    Iterator<Map.Entry<String, String>> entryIter = entries.iterator();
-    Map.Entry<String, String> entry;
-
-    entry = entryIter.next();
-    assertEquals("keyX", entry.getKey());
-    assertEquals("valueZ", entry.getValue());
-    entry = entryIter.next();
-    assertEquals("keyY", entry.getKey());
-    assertEquals("valueZ", entry.getValue());
-    entry = entryIter.next();
-    assertEquals("keyZ", entry.getKey());
-    assertEquals("valueZ", entry.getValue());
+    Map<String, String> map = types.mapXYZ();
+    Map<String, String> expectedMap = new HashMap<String, String>();
+    expectedMap.put("keyX", "valueZ");
+    expectedMap.put("keyY", "valueZ");
+    expectedMap.put("keyZ", "valueZ");
+    compareMapsComprehensively(map, expectedMap);
   }
 
   public void testConstantStringArrays() {
@@ -451,28 +447,6 @@
     assertEquals(Integer.MIN_VALUE, types.intMin());
   }
 
-  // Uncomment for desk tests
-  // /**
-  // * Tests focus on correctness of entries, since ABCD exercises the map.
-  // */
-  // public void testConstantMapEmpty() {
-  // TestConstants types = (TestConstants) GWT.create(TestConstants.class);
-  //
-  // ConstantMap map = (ConstantMap) types.mapEmpty();
-  //
-  // assertEquals(0, map.size());
-  //
-  // Set keys = map.keySet();
-  // assertEquals(0, keys.size());
-  // Iterator keyIter = keys.iterator();
-  // assertFalse(keyIter.hasNext());
-  //
-  // Collection values = map.values();
-  // assertEquals(0, values.size());
-  // Iterator valueIter = values.iterator();
-  // assertFalse(valueIter.hasNext());
-  // }
-
   public void testLocalizableInner() {
     // Check simple inner
     LocalizableSimpleInner s = (LocalizableSimpleInner) GWT.create(Inners.LocalizableSimpleInner.class);
@@ -522,8 +496,9 @@
 
     // Protected InnerClass
     InnerClass innerClass = new Inners.InnerClass();
-    String extendsAnotherInner = innerClass.testExtendsAnotherInner();
-    assertEquals("{innerInner=4.321, outer=outer}", extendsAnotherInner);
+    Map<String, String> extendsAnotherInner = innerClass.testExtendsAnotherInner();
+    assertEquals("4.321", extendsAnotherInner.get("innerInner"));
+    assertEquals("outer", extendsAnotherInner.get("outer"));
 
     // ExtendProtectedInner
     String extendProtectedInner = innerClass.testExtendsProtectedInner();
@@ -588,6 +563,47 @@
     }
   }
 
+  private <T> boolean compare(Collection<T> collection1,
+      Collection<T> collection2) {
+    if (collection1 == null) {
+      return (collection2 == null);
+    }
+    if (collection2 == null) {
+      return false;
+    }
+    if (collection1.size() != collection2.size()) {
+      return false;
+    }
+    for (T element1 : collection1) {
+      boolean found = false;
+      for (T element2 : collection2) {
+        if (element1.equals(element2)) {
+          found = true;
+          break;
+        }
+      }
+      if (!found) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // compare the map, entrySet, keySet, and values
+  private void compareMapsComprehensively(Map<String, String> map,
+      Map<String, String> expectedMap) {
+    // checking both directions to verify that the equals implementation is
+    // correct both ways
+    assertEquals(expectedMap, map);
+    assertEquals(map, expectedMap);
+    assertEquals(expectedMap.entrySet(), map.entrySet());
+    assertEquals(map.entrySet(), expectedMap.entrySet());
+    assertEquals(expectedMap.keySet(), map.keySet());
+    assertEquals(map.keySet(), expectedMap.keySet());
+    assertTrue(compare(expectedMap.values(), map.values()));
+    assertTrue(compare(map.values(), expectedMap.values()));
+  }
+
   private native void createDummyDictionaries() /*-{
     $wnd.testDic = new Object();
     $wnd.testDic.formattedMessage = "3 {2},{2},{2}, one {0}, two {1} {1}";
@@ -597,4 +613,12 @@
     $wnd.emptyDic = new Object();
     $wnd.malformedDic = 4;
   }-*/;
+
+  private Map<String, String> getMapFromArrayUsingASimpleRule(String array[]) {
+    Map<String, String> map = new HashMap<String, String>();
+    for (String str : array) {
+      map.put("key" + str, "value" + str);
+    }
+    return map;
+  }
 }
diff --git a/user/test/com/google/gwt/i18n/client/TestConstants.java b/user/test/com/google/gwt/i18n/client/TestConstants.java
index 0884f6e..e97674a 100644
--- a/user/test/com/google/gwt/i18n/client/TestConstants.java
+++ b/user/test/com/google/gwt/i18n/client/TestConstants.java
@@ -23,100 +23,98 @@
  */
 public interface TestConstants extends com.google.gwt.i18n.client.Constants {
 
-  /**
-   * @gwt.key string
-   */
-  String getString();
+  boolean booleanFalse();
 
-  String stringTrimsLeadingWhitespace();
-
-  String stringDoesNotTrimTrailingThreeSpaces();
-
-  String stringEmpty();
-
-  String stringJapaneseRed();
-
-  String stringJapaneseGreen();
-
-  String stringJapaneseBlue();
-
-  int intZero();
-
-  int intOne();
-
-  int intNegOne();
-
-  int intMax();
-
-  int intMin();
-
-  float floatPi();
-
-  float floatZero();
-
-  float floatOne();
-
-  float floatNegOne();
-
-  float floatPosMax();
-
-  float floatPosMin();
-
-  float floatNegMax();
-
-  float floatNegMin();
-
-  double doublePi();
-
-  double doubleZero();
-
-  double doubleOne();
-
-  double doubleNegOne();
-
-  double doublePosMax();
-
-  double doublePosMin();
+  boolean booleanTrue();
 
   double doubleNegMax();
 
   double doubleNegMin();
 
-  String[] stringArrayABCDEFG();
+  double doubleNegOne();
 
-  String[] stringArraySizeOneEmptyString();
+  double doubleOne();
 
-  String[] stringArraySizeOneX();
+  double doublePi();
 
-  String[] stringArraySizeTwoBothEmpty();
+  double doublePosMax();
 
-  String[] stringArraySizeThreeAllEmpty();
+  double doublePosMin();
 
-  String[] stringArraySizeTwoWithEscapedComma();
+  double doubleZero();
 
-  String[] stringArraySizeOneWithBackslashX();
+  float floatNegMax();
 
-  String[] stringArraySizeThreeWithDoubleBackslash();
+  float floatNegMin();
 
-  boolean booleanFalse();
+  float floatNegOne();
 
-  boolean booleanTrue();
+  float floatOne();
+
+  float floatPi();
+
+  float floatPosMax();
+
+  float floatPosMin();
+
+  float floatZero();
+
+  /**
+   * @gwt.key string
+   */
+  String getString();
+
+  int intMax();
+
+  int intMin();
+
+  int intNegOne();
+
+  int intOne();
+
+  int intZero();
 
   Map<String, String> mapABCD();
 
-  // raw type test
-  @SuppressWarnings("unchecked")
-  Map mapDCBA();
-
   Map<String, String> mapBACD();
 
   Map<String, String> mapBBB();
 
+  // raw type test
+  @SuppressWarnings("unchecked")
+  Map mapDCBA();
+
+  Map<String, String> mapEmpty();
+
+  // Map<String, String> mapWithMissingKey();
+
   Map<String, String> mapXYZ();
 
-  // uncomment for desk tests
-  // Map mapWithMissingKey();
+  String[] stringArrayABCDEFG();
 
-  // uncomment for desk tests
-  // Map mapEmpty();
+  String[] stringArraySizeOneEmptyString();
+
+  String[] stringArraySizeOneWithBackslashX();
+
+  String[] stringArraySizeOneX();
+
+  String[] stringArraySizeThreeAllEmpty();
+
+  String[] stringArraySizeThreeWithDoubleBackslash();
+
+  String[] stringArraySizeTwoBothEmpty();
+
+  String[] stringArraySizeTwoWithEscapedComma();
+
+  String stringDoesNotTrimTrailingThreeSpaces();
+
+  String stringEmpty();
+
+  String stringJapaneseBlue();
+
+  String stringJapaneseGreen();
+
+  String stringJapaneseRed();
+
+  String stringTrimsLeadingWhitespace();
 }
diff --git a/user/test/com/google/gwt/i18n/client/TestConstants.properties b/user/test/com/google/gwt/i18n/client/TestConstants.properties
index aeb7d51..35d7acf 100644
--- a/user/test/com/google/gwt/i18n/client/TestConstants.properties
+++ b/user/test/com/google/gwt/i18n/client/TestConstants.properties
@@ -81,7 +81,7 @@
 # mapXYZ: "keyX", "keyY", "keyZ"
 
 # map with a missing key (uncomment during desk tests; won't work in JUnit)
-# mapWithMissingKey: keyA, , keyB 
+# mapWithMissingKey: keyA, asdf, keyB 
 
-# empty map (uncomment during desk tests; won't work in JUnit)
-# mapEmpty: 
+# empty map
+mapEmpty: 
diff --git a/user/test/com/google/gwt/i18n/client/resolutiontest/Inners.java b/user/test/com/google/gwt/i18n/client/resolutiontest/Inners.java
index 566bdad..8abbc8b 100644
--- a/user/test/com/google/gwt/i18n/client/resolutiontest/Inners.java
+++ b/user/test/com/google/gwt/i18n/client/resolutiontest/Inners.java
@@ -140,10 +140,10 @@
     }
 
     /** Tests Protected Inner Class. */
-    public String testExtendsAnotherInner() {
+    public Map<String, String> testExtendsAnotherInner() {
       ExtendsAnotherInner clazz = (ExtendsAnotherInner) GWT.create(ExtendsAnotherInner.class);
-      Map answer = clazz.extendsAnotherInner();
-      return answer.toString();
+      Map<String, String> answer = clazz.extendsAnotherInner();
+      return answer;
     }
 
     /** Test for ExtendProtectedInner. */
@@ -164,7 +164,7 @@
         }
 
         /** Test maps in extension. */
-        Map extendsAnotherInner();
+        Map<String, String> extendsAnotherInner();
       }
 
       /**
diff --git a/user/test/com/google/gwt/user/client/ui/DomEventBenchmark.java b/user/test/com/google/gwt/user/client/ui/DomEventBenchmark.java
new file mode 100644
index 0000000..c335615
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/DomEventBenchmark.java
@@ -0,0 +1,131 @@
+/*
+ * 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.user.client.ui;
+
+import com.google.gwt.benchmarks.client.Benchmark;
+import com.google.gwt.benchmarks.client.IntRange;
+import com.google.gwt.benchmarks.client.Operator;
+import com.google.gwt.benchmarks.client.RangeField;
+import com.google.gwt.benchmarks.client.Setup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Measures the speed with which event handlers can be added and removed to a
+ * few simple UI classes. This is here to allow us to compare the performance of
+ * the old event listeners and the new event handlers. This first version, of
+ * course, can only look at listeners, as handlers aren't here yet.
+ */
+public class DomEventBenchmark extends Benchmark {
+
+  /**
+   * Whether to use old listeners or new handlers.
+   */
+  // protected enum RegistrationStyle {
+  // OLD_LISTENERS, NEW_HANDLERS
+  // }
+  private static final int NUM_WIDGETS = 250;
+
+  protected final IntRange listenerRange =
+      new IntRange(4, 400, Operator.MULTIPLY, 10);
+
+  private List<SimpleCheckBox> widgets;
+
+  private List<ClickListener> listeners;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.User";
+  }
+
+  // Required for JUnit
+  public void testSimpleCheckBoxAddAndRemoveForClicks() {
+  }
+
+  @Setup("reset")
+  public void testSimpleCheckBoxAddAndRemoveForClicks(
+  // @RangeEnum(RegistrationStyle.class) RegistrationStyle style,
+      @RangeField("listenerRange")
+      Integer numListeners) {
+
+    // The RegistrationStyle blank is here to be filled in when handlers arrive.
+    // Until then, just run the tests twice.
+
+    // if (RegistrationStyle.OLD_LISTENERS == style) {
+    for (SimpleCheckBox cb : widgets) {
+      for (int i = 0; i < numListeners; i++) {
+        cb.addClickListener(listeners.get(i));
+      }
+    }
+    for (SimpleCheckBox cb : widgets) {
+      for (int i = 0; i < numListeners; i++) {
+        cb.removeClickListener(listeners.get(i));
+      }
+    }
+    // }
+  }
+
+  // Required for JUnit
+  public void testSimpleCheckBoxAddForClicks() {
+  }
+
+  @Setup("reset")
+  public void testSimpleCheckBoxAddForClicks(
+  // @RangeEnum(RegistrationStyle.class) RegistrationStyle style,
+      @RangeField("listenerRange")
+      Integer numListeners) {
+
+    // The RegistrationStyle blank is here to be filled in when handlers arrive.
+    // Until then, just run the tests twice.
+
+    // if (RegistrationStyle.OLD_LISTENERS == style) {
+    for (SimpleCheckBox cb : widgets) {
+      for (int i = 0; i < numListeners; i++) {
+        cb.addClickListener(listeners.get(i));
+      }
+    }
+    // }
+  }
+
+  void reset(/* RegistrationStyle style , */Integer numListeners) {
+    RootPanel root = RootPanel.get();
+    root.clear();
+    widgets = new ArrayList<SimpleCheckBox>();
+    listeners = new ArrayList<ClickListener>();
+
+    for (int i = 0; i < NUM_WIDGETS; i++) {
+      SimpleCheckBox cb = new SimpleCheckBox();
+      widgets.add(cb);
+      root.add(cb);
+    }
+
+    for (int i = 0; i < numListeners; i++) {
+      listeners.add(new ClickListener() {
+        public void onClick(Widget sender) {
+        }
+      });
+    }
+  }
+
+  // /**
+  // * Cannot do this until we fix our inability to synthesize events,
+  // * pending...
+  // */
+  // public void testDispatch() {
+  //     
+  // }
+}
diff --git a/user/test/com/google/gwt/user/server/rpc/MockHttpServletRequest.java b/user/test/com/google/gwt/user/server/rpc/MockHttpServletRequest.java
index 85fb169..8cd0375 100644
--- a/user/test/com/google/gwt/user/server/rpc/MockHttpServletRequest.java
+++ b/user/test/com/google/gwt/user/server/rpc/MockHttpServletRequest.java
@@ -37,7 +37,7 @@
     throw new UnsupportedOperationException();
   }
 
-  public Enumeration getAttributeNames() {
+  public Enumeration<String> getAttributeNames() {
     throw new UnsupportedOperationException();
   }
 
@@ -73,11 +73,11 @@
     throw new UnsupportedOperationException();
   }
 
-  public Enumeration getHeaderNames() {
+  public Enumeration<String> getHeaderNames() {
     throw new UnsupportedOperationException();
   }
 
-  public Enumeration getHeaders(String arg0) {
+  public Enumeration<String> getHeaders(String arg0) {
     throw new UnsupportedOperationException();
   }
 
@@ -97,7 +97,7 @@
     throw new UnsupportedOperationException();
   }
 
-  public Enumeration getLocales() {
+  public Enumeration<Locale> getLocales() {
     throw new UnsupportedOperationException();
   }
 
@@ -117,11 +117,11 @@
     throw new UnsupportedOperationException();
   }
 
-  public Map getParameterMap() {
+  public Map<String, String[]> getParameterMap() {
     throw new UnsupportedOperationException();
   }
 
-  public Enumeration getParameterNames() {
+  public Enumeration<String> getParameterNames() {
     throw new UnsupportedOperationException();
   }
 
diff --git a/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTest.java b/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTest.java
index c881d1b..6914928 100644
--- a/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTest.java
+++ b/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTest.java
@@ -55,7 +55,7 @@
   private class MockHttpServletRequestContextPath extends
       MockHttpServletRequest {
     private String contextPath;
-    
+
     @Override
     public String getContextPath() {
       return contextPath;
@@ -65,11 +65,15 @@
   private class MockServletConfig implements ServletConfig {
     private ServletContext context;
 
+    public MockServletConfig(ServletContext context) {
+      this.context = context;
+    }
+
     public String getInitParameter(String arg0) {
       throw new UnsupportedOperationException();
     }
 
-    public Enumeration getInitParameterNames() {
+    public Enumeration<String> getInitParameterNames() {
       throw new UnsupportedOperationException();
     }
 
@@ -78,17 +82,11 @@
     }
 
     public String getServletName() {
-      throw new UnsupportedOperationException();
-    }
-
-    void setContext(ServletContext context) {
-      this.context = context;
+      return "MockServlet";
     }
   }
 
   private class MockServletContext implements ServletContext {
-    private ServletConfig config;
-    private Throwable exLogged;
     private String messageLogged;
 
     public MockServletContext() {
@@ -98,7 +96,7 @@
       throw new UnsupportedOperationException();
     }
 
-    public Enumeration getAttributeNames() {
+    public Enumeration<String> getAttributeNames() {
       throw new UnsupportedOperationException();
     }
 
@@ -110,7 +108,7 @@
       throw new UnsupportedOperationException();
     }
 
-    public Enumeration getInitParameterNames() {
+    public Enumeration<String> getInitParameterNames() {
       throw new UnsupportedOperationException();
     }
 
@@ -146,7 +144,7 @@
       throw new UnsupportedOperationException();
     }
 
-    public Set getResourcePaths(String arg0) {
+    public Set<String> getResourcePaths(String arg0) {
       throw new UnsupportedOperationException();
     }
 
@@ -162,11 +160,11 @@
       throw new UnsupportedOperationException();
     }
 
-    public Enumeration getServletNames() {
+    public Enumeration<String> getServletNames() {
       throw new UnsupportedOperationException();
     }
 
-    public Enumeration getServlets() {
+    public Enumeration<String> getServlets() {
       throw new UnsupportedOperationException();
     }
 
@@ -180,7 +178,6 @@
 
     public void log(String arg0, Throwable arg1) {
       messageLogged = arg0;
-      exLogged = arg1;
     }
 
     public void removeAttribute(String arg0) {
@@ -189,22 +186,16 @@
     public void setAttribute(String arg0, Object arg1) {
       throw new UnsupportedOperationException();
     }
-
-    void setConfig(ServletConfig config) {
-      this.config = config;
-    }
   }
 
   public void testDoGetSerializationPolicy_FailToOpenMD5Resource()
       throws ServletException {
-    MockServletConfig mockConfig = new MockServletConfig();
     MockServletContext mockContext = new MockServletContext() {
       public InputStream getResourceAsStream(String resource) {
         return null;
       }
     };
-    mockConfig.context = mockContext;
-    mockContext.config = mockConfig;
+    MockServletConfig mockConfig = new MockServletConfig(mockContext);
 
     RemoteServiceServlet rss = new RemoteServiceServlet();
 
@@ -229,10 +220,8 @@
    */
   public void testDoGetSerializationPolicy_ModuleInSeparateServlet()
       throws ServletException {
-    MockServletConfig mockConfig = new MockServletConfig();
     MockServletContext mockContext = new MockServletContext();
-    mockConfig.context = mockContext;
-    mockContext.config = mockConfig;
+    MockServletConfig mockConfig = new MockServletConfig(mockContext);
 
     RemoteServiceServlet rss = new RemoteServiceServlet();
 
@@ -257,7 +246,6 @@
       SerializationException {
     final String resourceHash = "12345";
     final String resourcePath = SerializationPolicyLoader.getSerializationPolicyFileName(resourceHash);
-    MockServletConfig mockConfig = new MockServletConfig();
     MockServletContext mockContext = new MockServletContext() {
       public InputStream getResourceAsStream(String resource) {
         if (resourcePath.equals(resource)) {
@@ -274,8 +262,7 @@
         return null;
       }
     };
-    mockConfig.context = mockContext;
-    mockContext.config = mockConfig;
+    MockServletConfig mockConfig = new MockServletConfig(mockContext);
 
     RemoteServiceServlet rss = new RemoteServiceServlet();
 
@@ -298,16 +285,18 @@
     assertNotValidDeserialize(serializationPolicy, Baz.class);
   }
 
-  private void assertDeserializeFields(SerializationPolicy policy, Class clazz) {
+  private void assertDeserializeFields(SerializationPolicy policy,
+      Class<?> clazz) {
     assertTrue(policy.shouldDeserializeFields(clazz));
   }
 
   private void assertNotDeserializeFields(SerializationPolicy policy,
-      Class clazz) {
+      Class<?> clazz) {
     assertFalse(policy.shouldDeserializeFields(clazz));
   }
 
-  private void assertNotValidDeserialize(SerializationPolicy policy, Class clazz) {
+  private void assertNotValidDeserialize(SerializationPolicy policy,
+      Class<?> clazz) {
     try {
       policy.validateDeserialize(clazz);
       fail("assertNotValidDeserialize: " + clazz.getName()
@@ -317,7 +306,7 @@
     }
   }
 
-  private void assertValidDeserialize(SerializationPolicy policy, Class clazz)
+  private void assertValidDeserialize(SerializationPolicy policy, Class<?> clazz)
       throws SerializationException {
     policy.validateDeserialize(clazz);
   }