Add mix-in behavior to the LinkerContext visible to the Linkers.
Adds an <extend-linker-context> gwt.xml element.
Filter generated resources with a partial path starting with no-deploy/
Add a test case for the no-deploy shim.
Addresses issue 2137.

Patch by: bobv
Review by: bruce, scottb


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2086 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/cfg/Messages.java b/dev/core/src/com/google/gwt/dev/cfg/Messages.java
index a15b208..a232c18 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/Messages.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/Messages.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * 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
@@ -28,6 +28,9 @@
   public static final Message2ClassClass INVALID_CLASS_DERIVATION = new Message2ClassClass(
       TreeLogger.ERROR, "Class '$0' must derive from '$1'");
 
+  public static final Message1String LINKER_NAME_INVALID = new Message1String(
+      TreeLogger.ERROR, "Invalid linker name '$0'");
+
   public static final Message1String PROPERTY_NAME_INVALID = new Message1String(
       TreeLogger.ERROR, "Invalid property name '$0'");
 
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 e224fd2..2ab0fd6 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
@@ -23,11 +23,12 @@
 import com.google.gwt.dev.jdt.TypeOracleBuilder;
 import com.google.gwt.dev.jdt.URLCompilationUnitProvider;
 import com.google.gwt.dev.linker.Linker;
+import com.google.gwt.dev.linker.LinkerContextShim;
 import com.google.gwt.dev.util.Empty;
 import com.google.gwt.dev.util.FileOracle;
 import com.google.gwt.dev.util.FileOracleFactory;
-import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.PerfLogger;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.FileOracleFactory.FileFilter;
 
 import org.apache.tools.ant.types.ZipScanner;
@@ -97,9 +98,9 @@
 
   private TypeOracle lazyTypeOracle;
 
-  private final Map<String, Linker> linkersByName = new HashMap<String, Linker>();
+  private final List<Class<? extends LinkerContextShim>> linkerContextShimTypes = new ArrayList<Class<? extends LinkerContextShim>>();
 
-  private final Map<String, String> linkerTypesByName = new HashMap<String, String>();
+  private final Map<String, Linker> linkersByName = new HashMap<String, Linker>();
 
   private final long moduleDefCreationTime = System.currentTimeMillis();
 
@@ -131,8 +132,19 @@
     gwtXmlFiles.add(xmlFile);
   }
 
-  public void addLinker(String name, String className) {
-    linkerTypesByName.put(name, className);
+  public void addLinker(String name, Linker linker) {
+    linkersByName.put(name, linker);
+  }
+
+  public void addLinkerContextShim(Class<? extends LinkerContextShim> clazz) {
+    /*
+     * It's possible a shim may be registered more than once, so this check is
+     * used to de-duplicate the final list, which will reflect the order of the
+     * first appearance of any LinkerContextShim type.
+     */
+    if (!linkerContextShimTypes.contains(clazz)) {
+      linkerContextShimTypes.add(clazz);
+    }
   }
 
   public synchronized void addPublicPackage(String publicPackage,
@@ -255,6 +267,10 @@
     return linkersByName.get(name);
   }
 
+  public List<Class<? extends LinkerContextShim>> getLinkerContextShims() {
+    return linkerContextShimTypes;
+  }
+
   public synchronized String getName() {
     return name;
   }
@@ -419,43 +435,21 @@
     branch = Messages.PUBLIC_PATH_LOCATIONS.branch(logger, null);
     lazyPublicOracle = publicPathEntries.create(branch);
 
-    /*
-     * Validate all linkers before we go off and compile something. badLinker is
-     * checked at the end of the method so we can maximize the number of Linkers
-     * to report errors about before exiting.
-     * 
-     * It is not legal to have zero linkers defined
-     */
-    boolean badLinker = false;
-    for (Map.Entry<String, String> entry : linkerTypesByName.entrySet()) {
-      try {
-        Class<?> clazz = Class.forName(entry.getValue());
-        Class<? extends Linker> linkerClazz = clazz.asSubclass(Linker.class);
-        linkersByName.put(entry.getKey(), linkerClazz.newInstance());
-      } catch (ClassCastException e) {
-        logger.log(TreeLogger.ERROR, "Not actually a Linker", e);
-        badLinker = true;
-      } catch (ClassNotFoundException e) {
-        logger.log(TreeLogger.ERROR, "Unable to find Linker", e);
-        badLinker = true;
-      } catch (InstantiationException e) {
-        logger.log(TreeLogger.ERROR, "Unable to create Linker", e);
-        badLinker = true;
-      } catch (IllegalAccessException e) {
-        logger.log(TreeLogger.ERROR,
-            "Linker does not have a public constructor", e);
-        badLinker = true;
-      }
-    }
-
+    boolean fail = false;
     for (String linkerName : activeLinkerNames) {
       if (!linkersByName.containsKey(linkerName)) {
         logger.log(TreeLogger.ERROR, "Unknown linker name " + linkerName, null);
-        badLinker = true;
+        fail = true;
       }
     }
 
-    if (badLinker) {
+    if (linkersByName.size() == 0 || activeLinkerNames.length == 0) {
+      logger.log(TreeLogger.ERROR, "At least one Linker must be defind and "
+          + "at least one Linker must be active.", null);
+      fail = true;
+    }
+
+    if (fail) {
       throw new UnableToCompleteException();
     }
 
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 c6698bf..09f2561 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * 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
@@ -25,6 +25,8 @@
 import com.google.gwt.dev.js.ast.JsFunction;
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.linker.Linker;
+import com.google.gwt.dev.linker.LinkerContextShim;
 import com.google.gwt.dev.util.Empty;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.xml.AttributeConverter;
@@ -33,6 +35,7 @@
 import java.io.IOException;
 import java.io.StringReader;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -56,6 +59,8 @@
 
     protected final String __entry_point_1_class = null;
 
+    protected final String __extend_linker_context_1_class = null;
+
     protected final String __extend_property_1_name = null;
 
     protected final String __extend_property_2_values = null;
@@ -114,15 +119,9 @@
 
     private Schema fChild;
 
-    protected Schema __define_linker_begin(String name, String className)
+    protected Schema __define_linker_begin(LinkerName name, Linker linker)
         throws UnableToCompleteException {
-      if (!Util.isValidJavaIdent(name)) {
-        logger.log(TreeLogger.ERROR, "Linker names must be valid identifiers",
-            null);
-        throw new UnableToCompleteException();
-      }
-
-      moduleDef.addLinker(name, className);
+      moduleDef.addLinker(name.name, linker);
       return null;
     }
 
@@ -149,6 +148,18 @@
       return null;
     }
 
+    protected Schema __extend_linker_context_begin(Class<?> clazz)
+        throws UnableToCompleteException {
+      try {
+        moduleDef.addLinkerContextShim(clazz.asSubclass(LinkerContextShim.class));
+        return null;
+      } catch (ClassCastException e) {
+        Messages.INVALID_CLASS_DERIVATION.log(logger, clazz,
+            LinkerContextShim.class, null);
+        throw new UnableToCompleteException();
+      }
+    }
+
     protected Schema __extend_property_begin(Property property,
         PropertyValue[] values) {
       for (int i = 0; i < values.length; i++) {
@@ -279,8 +290,12 @@
       return null;
     }
 
-    protected Schema __set_linker_begin(String name) {
-      moduleDef.setActiveLinkerNames(name.split(","));
+    protected Schema __set_linker_begin(LinkerName[] names) {
+      String[] asString = new String[names.length];
+      for (int i = 0; i < names.length; i++) {
+        asString[i] = names[i].name;
+      }
+      moduleDef.setActiveLinkerNames(asString);
       return null;
     }
 
@@ -451,6 +466,23 @@
     }
   }
 
+  /**
+   * Processes attributes of java.lang.Class type.
+   */
+  private final class ClassAttrCvt extends AttributeConverter {
+    @Override
+    public Object convertToArg(Schema schema, int line, String elem,
+        String attr, String value) throws UnableToCompleteException {
+      try {
+        ClassLoader cl = Thread.currentThread().getContextClassLoader();
+        return cl.loadClass(value);
+      } catch (ClassNotFoundException e) {
+        Messages.UNABLE_TO_LOAD_CLASS.log(logger, value, e);
+        throw new UnableToCompleteException();
+      }
+    }
+  }
+
   private final class ConditionSchema extends Schema {
 
     protected final String __when_property_is_1_name = null;
@@ -543,15 +575,66 @@
     }
   }
 
+  private static class LinkerName {
+    public final String name;
+
+    public LinkerName(String name) {
+      this.name = name;
+    }
+  }
+
+  /**
+   * Converts a string into a linker name, validating it in the process.
+   */
+  private final class LinkerNameArrayAttrCvt extends AttributeConverter {
+
+    public Object convertToArg(Schema schema, int line, String elem,
+        String attr, String value) throws UnableToCompleteException {
+      String[] tokens = value.split(",");
+      List<LinkerName> toReturn = new ArrayList<LinkerName>(tokens.length);
+
+      for (String token : tokens) {
+        token = token.trim();
+        if (moduleDef.getLinker(token) == null) {
+          Messages.LINKER_NAME_INVALID.log(logger, token, null);
+          throw new UnableToCompleteException();
+        }
+
+        toReturn.add(new LinkerName(token));
+      }
+
+      // It is a valid list of names.
+      return toReturn.toArray(new LinkerName[tokens.length]);
+    }
+  }
+
+  /**
+   * Converts a string into a linker name, validating it in the process.
+   */
+  private final class LinkerNameAttrCvt extends AttributeConverter {
+
+    public Object convertToArg(Schema schema, int line, String elem,
+        String attr, String value) throws UnableToCompleteException {
+      // Ensure the value is a valid Java identifier
+      if (!Util.isValidJavaIdent(value)) {
+        Messages.LINKER_NAME_INVALID.log(logger, value, null);
+        throw new UnableToCompleteException();
+      }
+
+      // It is a valid name.
+      return new LinkerName(value);
+    }
+  }
+
   /**
    * Creates singleton instances of objects based on an attribute containing a
    * class name.
    */
-  private final class ObjAttrCvt extends AttributeConverter {
+  private final class ObjAttrCvt<T> extends AttributeConverter {
 
-    private final Class fReqdSuperclass;
+    private final Class<T> fReqdSuperclass;
 
-    public ObjAttrCvt(Class reqdSuperclass) {
+    public ObjAttrCvt(Class<T> reqdSuperclass) {
       fReqdSuperclass = reqdSuperclass;
     }
 
@@ -565,23 +648,21 @@
         return found;
       }
 
+      Class<?> foundClass = null;
       try {
         // Load the class.
         //
         ClassLoader cl = Thread.currentThread().getContextClassLoader();
-        Class clazz = cl.loadClass(attrValue);
+        foundClass = cl.loadClass(attrValue);
+        Class<? extends T> clazz = foundClass.asSubclass(fReqdSuperclass);
 
-        // Make sure it's compatible.
-        //
-        if (!fReqdSuperclass.isAssignableFrom(clazz)) {
-          Messages.INVALID_CLASS_DERIVATION.log(logger, clazz, fReqdSuperclass,
-              null);
-          throw new UnableToCompleteException();
-        }
-
-        Object object = clazz.newInstance();
+        T object = clazz.newInstance();
         singletonsByName.put(attrValue, object);
         return object;
+      } catch (ClassCastException e) {
+        Messages.INVALID_CLASS_DERIVATION.log(logger, foundClass,
+            fReqdSuperclass, null);
+        throw new UnableToCompleteException();
       } catch (ClassNotFoundException e) {
         Messages.UNABLE_TO_LOAD_CLASS.log(logger, attrValue, e);
         throw new UnableToCompleteException();
@@ -747,7 +828,7 @@
     }
   }
 
-  private static final Map singletonsByName = new HashMap();
+  private static final Map<String, Object> singletonsByName = new HashMap<String, Object>();
 
   private static void addPrefix(String[] strings, String prefix) {
     for (int i = 0; i < strings.length; ++i) {
@@ -765,13 +846,19 @@
 
   private final BodySchema bodySchema;
 
+  private final ClassAttrCvt classAttrCvt = new ClassAttrCvt();
+
   private boolean foundAnyPublic;
 
   private boolean foundExplicitSourceOrSuperSource;
-
-  private final ObjAttrCvt genAttrCvt = new ObjAttrCvt(Generator.class);
+  private final ObjAttrCvt<Generator> genAttrCvt = new ObjAttrCvt<Generator>(
+      Generator.class);
   private final JsParser jsParser = new JsParser();
   private final JsProgram jsPgm = new JsProgram();
+  private final ObjAttrCvt<Linker> linkerAttrCvt = new ObjAttrCvt<Linker>(
+      Linker.class);
+  private final LinkerNameArrayAttrCvt linkerNameArrayAttrCvt = new LinkerNameArrayAttrCvt();
+  private final LinkerNameAttrCvt linkerNameAttrCvt = new LinkerNameAttrCvt();
   private final ModuleDefLoader loader;
   private final TreeLogger logger;
   private final ModuleDef moduleDef;
@@ -797,6 +884,10 @@
     registerAttributeConverter(PropertyValue.class, propValueAttrCvt);
     registerAttributeConverter(PropertyValue[].class, propValueArrayAttrCvt);
     registerAttributeConverter(Generator.class, genAttrCvt);
+    registerAttributeConverter(Linker.class, linkerAttrCvt);
+    registerAttributeConverter(LinkerName.class, linkerNameAttrCvt);
+    registerAttributeConverter(LinkerName[].class, linkerNameArrayAttrCvt);
+    registerAttributeConverter(Class.class, classAttrCvt);
   }
 
   protected Schema __module_begin() {
diff --git a/dev/core/src/com/google/gwt/dev/linker/LinkerContext.java b/dev/core/src/com/google/gwt/dev/linker/LinkerContext.java
index 4fa6d2c..7767d25 100644
--- a/dev/core/src/com/google/gwt/dev/linker/LinkerContext.java
+++ b/dev/core/src/com/google/gwt/dev/linker/LinkerContext.java
@@ -19,6 +19,7 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 
 import java.io.OutputStream;
+import java.util.Comparator;
 import java.util.SortedSet;
 
 /**
@@ -30,6 +31,60 @@
  */
 public interface LinkerContext {
   /**
+   * Orders CompilationResults by string comparison of their JavaScript.
+   */
+  Comparator<CompilationResult> COMPILATION_RESULT_COMPARATOR = new Comparator<CompilationResult>() {
+    public int compare(CompilationResult o1, CompilationResult o2) {
+      return o1.getJavaScript().compareTo(o2.getJavaScript());
+    }
+  };
+
+  /**
+   * Orders GeneratedResources by string comparison of their partial paths.
+   */
+  Comparator<GeneratedResource> GENERATED_RESOURCE_COMPARATOR = new Comparator<GeneratedResource>() {
+    public int compare(GeneratedResource o1, GeneratedResource o2) {
+      return o1.getPartialPath().compareTo(o2.getPartialPath());
+    }
+  };
+
+  /**
+   * Orders PublicResources by string comparison of their partial paths.
+   */
+  Comparator<PublicResource> PUBLIC_RESOURCE_COMPARATOR = new Comparator<PublicResource>() {
+    public int compare(PublicResource o1, PublicResource o2) {
+      return o1.getPartialPath().compareTo(o2.getPartialPath());
+    }
+  };
+
+  /**
+   * Orders ModuleScriptResources by string comparison of their src attributes.
+   */
+  Comparator<ModuleScriptResource> SCRIPT_RESOURCE_COMPARATOR = new Comparator<ModuleScriptResource>() {
+    public int compare(ModuleScriptResource o1, ModuleScriptResource o2) {
+      return o1.getSrc().compareTo(o2.getSrc());
+    }
+  };
+
+  /**
+   * Orders SelectionProperties by string comparison of their names.
+   */
+  Comparator<SelectionProperty> SELECTION_PROPERTY_COMPARATOR = new Comparator<SelectionProperty>() {
+    public int compare(SelectionProperty o1, SelectionProperty o2) {
+      return o1.getName().compareTo(o2.getName());
+    }
+  };
+
+  /**
+   * Orders ModuleStyleResources by string comparison of their src attributes.
+   */
+  Comparator<ModuleStylesheetResource> STYLE_RESOURCE_COMPARATOR = new Comparator<ModuleStylesheetResource>() {
+    public int compare(ModuleStylesheetResource o1, ModuleStylesheetResource o2) {
+      return o1.getSrc().compareTo(o2.getSrc());
+    }
+  };
+
+  /**
    * Finalizes the OutputStream for a given artifact. This method must be called
    * in order to actually place the artifact into the output directory. If the
    * OutptStream has not already been closed, this method will close the
diff --git a/dev/core/src/com/google/gwt/dev/linker/LinkerContextShim.java b/dev/core/src/com/google/gwt/dev/linker/LinkerContextShim.java
new file mode 100644
index 0000000..59b25c5
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/linker/LinkerContextShim.java
@@ -0,0 +1,124 @@
+/*
+ * 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.linker;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.io.OutputStream;
+import java.util.SortedSet;
+
+/**
+ * This base class allows behaviors to be injected into the
+ * {@link LinkerContext} that is observed by the {@link Linker} types operating
+ * on the output from the compiler. Instances of LinkerContextShim are mapped
+ * into the compilation process by including {@code <extend-linker-context>}
+ * tags in the GWT module definition. Subclasses of LinkerContextShim must
+ * define a two-argument constructor that accepts an instance of TreeLogger and
+ * LinkerContext.
+ * <p>
+ * The default behavior of all methods in this class is to delegate to the
+ * LinkerContext returned from {@link #getParent()}. Separate shim instances
+ * are guaranteed to be used for each Linker instance.
+ * <p>
+ * No guarantees are made on the order in which or number of times any method on
+ * the LinkerContextShim will be invoked. Implementations are encouraged to
+ * precompute all return values for each method in their constructors and return
+ * an unmodifiable wrapper around the collection using
+ * {@link java.util.Collections#unmodifiableSortedSet(SortedSet)}.
+ */
+public abstract class LinkerContextShim implements LinkerContext {
+  private final LinkerContext parent;
+
+  protected LinkerContextShim(TreeLogger logger, LinkerContext parent)
+      throws UnableToCompleteException {
+    this.parent = parent;
+  }
+
+  /**
+   * Finalize all actions performed by the LinkerContextShim. This method will
+   * be called in reverse order; it will not be called on a parent until all of
+   * its children have been committed.
+   */
+  public void commit(TreeLogger logger) throws UnableToCompleteException {
+  }
+
+  public void commit(TreeLogger logger, OutputStream out)
+      throws UnableToCompleteException {
+    getParent().commit(logger, out);
+  }
+
+  public SortedSet<CompilationResult> getCompilations() {
+    return getParent().getCompilations();
+  }
+
+  public SortedSet<GeneratedResource> getGeneratedResources() {
+    return getParent().getGeneratedResources();
+  }
+
+  public String getModuleFunctionName() {
+    return getParent().getModuleFunctionName();
+  }
+
+  public String getModuleName() {
+    return getParent().getModuleName();
+  }
+
+  public SortedSet<ModuleScriptResource> getModuleScripts() {
+    return getParent().getModuleScripts();
+  }
+
+  public SortedSet<ModuleStylesheetResource> getModuleStylesheets() {
+    return getParent().getModuleStylesheets();
+  }
+
+  /**
+   * Obtain a reference to the parent LinkerContext. This method is guaranteed
+   * to return a useful value before any of the other LinkerContext-derived
+   * methods are invoked.
+   */
+  // NB This is final because StandardLinkerContext depends on it to unwind
+  public final LinkerContext getParent() {
+    return parent;
+  }
+
+  public SortedSet<SelectionProperty> getProperties() {
+    return getParent().getProperties();
+  }
+
+  public SortedSet<PublicResource> getPublicResources() {
+    return getParent().getPublicResources();
+  }
+
+  public String optimizeJavaScript(TreeLogger logger, String jsProgram)
+      throws UnableToCompleteException {
+    return getParent().optimizeJavaScript(logger, jsProgram);
+  }
+
+  public OutputStream tryCreateArtifact(TreeLogger logger, String partialPath) {
+    return getParent().tryCreateArtifact(logger, partialPath);
+  }
+
+  public GeneratedResource tryGetGeneratedResource(TreeLogger logger,
+      String partialPath) {
+    return getParent().tryGetGeneratedResource(logger, partialPath);
+  }
+
+  public PublicResource tryGetPublicResource(TreeLogger logger,
+      String partialPath) {
+    return getParent().tryGetPublicResource(logger, partialPath);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/linker/NoDeployResourcesShim.java b/dev/core/src/com/google/gwt/dev/linker/NoDeployResourcesShim.java
new file mode 100644
index 0000000..fd49ccc
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/linker/NoDeployResourcesShim.java
@@ -0,0 +1,66 @@
+/*
+ * 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.linker;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.util.Collections;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * This class prevents generated resources whose partial path begins with
+ * {@value #PREFIX} from being visible.
+ */
+public class NoDeployResourcesShim extends LinkerContextShim {
+  public static final String PREFIX = "no-deploy/";
+  private final SortedSet<GeneratedResource> generatedResources;
+
+  public NoDeployResourcesShim(TreeLogger logger, LinkerContext parent)
+      throws UnableToCompleteException {
+    super(logger, parent);
+
+    SortedSet<GeneratedResource> mutableSet = new TreeSet<GeneratedResource>(
+        GENERATED_RESOURCE_COMPARATOR);
+
+    SortedSet<GeneratedResource> view = super.getGeneratedResources();
+    for (GeneratedResource res : view) {
+      if (!res.getPartialPath().toLowerCase().startsWith(PREFIX)) {
+        mutableSet.add(res);
+      } else {
+        logger.log(TreeLogger.SPAM, "Excluding generated resource "
+            + res.getPartialPath(), null);
+      }
+    }
+
+    assert mutableSet.size() <= view.size();
+
+    if (mutableSet.size() == view.size()) {
+      // Reuse the existing view
+      generatedResources = view;
+
+    } else {
+      // Ensure that the new view is immutable
+      generatedResources = Collections.unmodifiableSortedSet(mutableSet);
+    }
+  }
+
+  @Override
+  public SortedSet<GeneratedResource> getGeneratedResources() {
+    return generatedResources;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/linker/impl/StandardLinkerContext.java b/dev/core/src/com/google/gwt/dev/linker/impl/StandardLinkerContext.java
index 9786001..e038991 100644
--- a/dev/core/src/com/google/gwt/dev/linker/impl/StandardLinkerContext.java
+++ b/dev/core/src/com/google/gwt/dev/linker/impl/StandardLinkerContext.java
@@ -42,6 +42,7 @@
 import com.google.gwt.dev.linker.GeneratedResource;
 import com.google.gwt.dev.linker.Linker;
 import com.google.gwt.dev.linker.LinkerContext;
+import com.google.gwt.dev.linker.LinkerContextShim;
 import com.google.gwt.dev.linker.ModuleScriptResource;
 import com.google.gwt.dev.linker.ModuleStylesheetResource;
 import com.google.gwt.dev.linker.PublicResource;
@@ -55,12 +56,15 @@
 import java.io.OutputStream;
 import java.io.Reader;
 import java.io.StringReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
@@ -91,60 +95,6 @@
     }
   }
 
-  /**
-   * Orders CompilationResults by string comparison of their JavaScript.
-   */
-  public static final Comparator<CompilationResult> COMPILATION_RESULT_COMPARATOR = new Comparator<CompilationResult>() {
-    public int compare(CompilationResult o1, CompilationResult o2) {
-      return o1.getJavaScript().compareTo(o2.getJavaScript());
-    }
-  };
-
-  /**
-   * Orders GeneratedResources by string comparison of their partial paths.
-   */
-  public static final Comparator<GeneratedResource> GENERATED_RESOURCE_COMPARATOR = new Comparator<GeneratedResource>() {
-    public int compare(GeneratedResource o1, GeneratedResource o2) {
-      return o1.getPartialPath().compareTo(o2.getPartialPath());
-    }
-  };
-
-  /**
-   * Orders PublicResources by string comparison of their partial paths.
-   */
-  public static final Comparator<PublicResource> PUBLIC_RESOURCE_COMPARATOR = new Comparator<PublicResource>() {
-    public int compare(PublicResource o1, PublicResource o2) {
-      return o1.getPartialPath().compareTo(o2.getPartialPath());
-    }
-  };
-
-  /**
-   * Orders ModuleScriptResources by string comparison of their src attributes.
-   */
-  public static final Comparator<ModuleScriptResource> SCRIPT_RESOURCE_COMPARATOR = new Comparator<ModuleScriptResource>() {
-    public int compare(ModuleScriptResource o1, ModuleScriptResource o2) {
-      return o1.getSrc().compareTo(o2.getSrc());
-    }
-  };
-
-  /**
-   * Orders SelectionProperties by string comparison of their names.
-   */
-  public static final Comparator<SelectionProperty> SELECTION_PROPERTY_COMPARATOR = new Comparator<SelectionProperty>() {
-    public int compare(SelectionProperty o1, SelectionProperty o2) {
-      return o1.getName().compareTo(o2.getName());
-    }
-  };
-
-  /**
-   * Orders ModuleStyleResources by string comparison of their src attributes.
-   */
-  public static final Comparator<ModuleStylesheetResource> STYLE_RESOURCE_COMPARATOR = new Comparator<ModuleStylesheetResource>() {
-    public int compare(ModuleStylesheetResource o1, ModuleStylesheetResource o2) {
-      return o1.getSrc().compareTo(o2.getSrc());
-    }
-  };
-
   private final File compilationsDir;
   private final SortedSet<GeneratedResource> generatedResources;
   private final Map<String, GeneratedResource> generatedResourcesByName = new HashMap<String, GeneratedResource>();
@@ -173,10 +123,12 @@
   private final Map<String, PublicResource> publicResourcesByName = new HashMap<String, PublicResource>();
   private final Map<String, StandardCompilationResult> resultsByStrongName = new HashMap<String, StandardCompilationResult>();
   private final SortedSet<ModuleScriptResource> scriptResources;
+  private final List<Class<? extends LinkerContextShim>> shimClasses;
   private final SortedSet<ModuleStylesheetResource> stylesheetResources;
 
   public StandardLinkerContext(TreeLogger logger, ModuleDef module,
-      File outDir, File generatorDir, JJSOptions jjsOptions) {
+      File outDir, File generatorDir, JJSOptions jjsOptions)
+      throws UnableToCompleteException {
     logger = logger.branch(TreeLogger.DEBUG,
         "Constructing StandardLinkerContext", null);
 
@@ -184,6 +136,8 @@
     this.moduleFunctionName = module.getFunctionName();
     this.moduleName = module.getName();
     this.moduleOutDir = outDir;
+    this.shimClasses = new ArrayList<Class<? extends LinkerContextShim>>(
+        module.getLinkerContextShims());
 
     if (moduleOutDir != null) {
       compilationsDir = new File(moduleOutDir, ".gwt-compiler/compilations");
@@ -372,7 +326,46 @@
       logger = logger.branch(TreeLogger.INFO, "Linking compilation with "
           + linker.getDescription() + " Linker into " + linkerOutDir.getPath(),
           null);
-      linker.link(logger, this);
+
+      // Instantiate per-Linker instances of the LinkerContextShims
+      LinkerContext shimParent = this;
+      for (Class<? extends LinkerContextShim> clazz : shimClasses) {
+        TreeLogger shimLogger = logger.branch(TreeLogger.DEBUG,
+            "Constructing LinkerContextShim " + clazz.getName(), null);
+        try {
+          Constructor<? extends LinkerContextShim> constructor = clazz.getConstructor(
+              TreeLogger.class, LinkerContext.class);
+          shimParent = constructor.newInstance(shimLogger, shimParent);
+        } catch (InstantiationException e) {
+          shimLogger.log(TreeLogger.ERROR,
+              "Unable to create LinkerContextShim", e);
+          throw new UnableToCompleteException();
+        } catch (InvocationTargetException e) {
+          shimLogger.log(TreeLogger.ERROR,
+              "Unable to create LinkerContextShim", e);
+          throw new UnableToCompleteException();
+        } catch (NoSuchMethodException e) {
+          shimLogger.log(TreeLogger.ERROR,
+              "LinkerContextShim subtypes must implement a two-argument "
+                  + "constructor accepting a TreeLogger and a LinkerContext", e);
+          throw new UnableToCompleteException();
+        } catch (IllegalAccessException e) {
+          shimLogger.log(TreeLogger.ERROR,
+              "Unable to create LinkerContextShim", e);
+          throw new UnableToCompleteException();
+        }
+      }
+
+      linker.link(logger, shimParent);
+
+      // Unwind the LinkerContextShim stack
+      while (shimParent != this) {
+        LinkerContextShim shim = (LinkerContextShim) shimParent;
+        shim.commit(logger.branch(TreeLogger.DEBUG,
+            "Committing LinkerContextShim " + shim.getClass().getName(), null));
+        shimParent = shim.getParent();
+      }
+
     } finally {
       reset();
     }
diff --git a/user/src/com/google/gwt/core/Core.gwt.xml b/user/src/com/google/gwt/core/Core.gwt.xml
index 1487956..91aee75 100644
--- a/user/src/com/google/gwt/core/Core.gwt.xml
+++ b/user/src/com/google/gwt/core/Core.gwt.xml
@@ -24,5 +24,7 @@
    <define-linker name="sso" class="com.google.gwt.dev.linker.SingleScriptLinker" />
    <define-linker name="xs" class="com.google.gwt.dev.linker.XSLinker" />
    <set-linker name="std" />
+   
+   <!-- Filters generated resources in the no-deploy/ directory -->
+   <extend-linker-context class="com.google.gwt.dev.linker.NoDeployResourcesShim" />
 </module>
-
diff --git a/user/test/com/google/gwt/module/ModuleSuite.java b/user/test/com/google/gwt/module/ModuleSuite.java
index 422fa3c..ce6c509 100644
--- a/user/test/com/google/gwt/module/ModuleSuite.java
+++ b/user/test/com/google/gwt/module/ModuleSuite.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.junit.tools.GWTTestSuite;
 import com.google.gwt.module.client.DoubleScriptInjectionTest;
+import com.google.gwt.module.client.NoDeployTest;
 import com.google.gwt.module.client.SingleScriptInjectionTest;
 
 import junit.framework.Test;
@@ -27,10 +28,11 @@
 public class ModuleSuite {
   public static Test suite() {
     GWTTestSuite suite = new GWTTestSuite();
-    
+
     suite.addTestSuite(SingleScriptInjectionTest.class);
     suite.addTestSuite(DoubleScriptInjectionTest.class);
-    
+    suite.addTestSuite(NoDeployTest.class);
+
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/module/NoDeployTest.gwt.xml b/user/test/com/google/gwt/module/NoDeployTest.gwt.xml
new file mode 100644
index 0000000..a6af83f
--- /dev/null
+++ b/user/test/com/google/gwt/module/NoDeployTest.gwt.xml
@@ -0,0 +1,21 @@
+<!--                                                                        -->
+<!-- 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.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.core.Core"/>
+
+  <generate-with class="com.google.gwt.module.rebind.NoDeployGenerator" >
+    <when-type-assignable class="com.google.gwt.module.client.NoDeployTest.NoDeploy" />
+  </generate-with>
+</module>
diff --git a/user/test/com/google/gwt/module/client/NoDeployTest.java b/user/test/com/google/gwt/module/client/NoDeployTest.java
new file mode 100644
index 0000000..eb4038b
--- /dev/null
+++ b/user/test/com/google/gwt/module/client/NoDeployTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.module.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Ensure that generated resources in the no-deploy directory aren't in the
+ * output. This validates that the
+ * {@link com.google.gwt.dev.linker.NoDeployResourcesShim} is being loaded and
+ * operating correctly.
+ */
+public class NoDeployTest extends GWTTestCase {
+
+  /**
+   * Used only to trigger the NoDeployGenerator.
+   */
+  private static class NoDeploy {
+  }
+
+  public static final String TEST_TEXT = "Hello world!";
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.module.NoDeployTest";
+  }
+
+  public void testDeploy() throws RequestException {
+    GWT.create(NoDeploy.class);
+
+    // Try fetching a file that should exist
+    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
+        GWT.getHostPageBaseURL() + "deploy/exists.txt");
+    delayTestFinish(500);
+    builder.sendRequest("", new RequestCallback() {
+
+      public void onError(Request request, Throwable exception) {
+        fail();
+      }
+
+      public void onResponseReceived(Request request, Response response) {
+        assertEquals(TEST_TEXT, response.getText());
+        finishTest();
+      }
+    });
+  }
+
+  public void testNoDeploy() throws RequestException {
+    if (!GWT.isScript()) {
+      // LinkerContextShims aren't used in hosted-mode
+      return;
+    }
+
+    GWT.create(NoDeploy.class);
+
+    // Try fetching a file that shouldn't exist
+    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
+        GWT.getHostPageBaseURL() + "no-deploy/inGenerated.txt");
+    delayTestFinish(500);
+    builder.sendRequest("", new RequestCallback() {
+
+      public void onError(Request request, Throwable exception) {
+        fail();
+      }
+
+      public void onResponseReceived(Request request, Response response) {
+        assertEquals(404, response.getStatusCode());
+        finishTest();
+      }
+    });
+  }
+
+  /**
+   * Verify that a no-deploy directory in the public path will be deployed.
+   */
+  public void testNoDeployInPublic() throws RequestException {
+    GWT.create(NoDeploy.class);
+
+    // Try fetching a file that shouldn't exist
+    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
+        GWT.getHostPageBaseURL() + "no-deploy/inPublic.txt");
+    delayTestFinish(500);
+    builder.sendRequest("", new RequestCallback() {
+
+      public void onError(Request request, Throwable exception) {
+        fail();
+      }
+
+      public void onResponseReceived(Request request, Response response) {
+        assertEquals(TEST_TEXT, response.getText());
+        finishTest();
+      }
+    });
+  }
+}
diff --git a/user/test/com/google/gwt/module/public/no-deploy/inPublic.txt b/user/test/com/google/gwt/module/public/no-deploy/inPublic.txt
new file mode 100644
index 0000000..6769dd6
--- /dev/null
+++ b/user/test/com/google/gwt/module/public/no-deploy/inPublic.txt
@@ -0,0 +1 @@
+Hello world!
\ No newline at end of file
diff --git a/user/test/com/google/gwt/module/rebind/NoDeployGenerator.java b/user/test/com/google/gwt/module/rebind/NoDeployGenerator.java
new file mode 100644
index 0000000..46b1d94
--- /dev/null
+++ b/user/test/com/google/gwt/module/rebind/NoDeployGenerator.java
@@ -0,0 +1,60 @@
+/*
+ * 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.module.rebind;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.linker.NoDeployResourcesShim;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.module.client.NoDeployTest;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Creates two files in the generated output directory.
+ */
+public class NoDeployGenerator extends Generator {
+
+  @Override
+  public String generate(TreeLogger logger, GeneratorContext context,
+      String typeName) throws UnableToCompleteException {
+
+    try {
+      createFile(logger, context, "deploy/exists.txt");
+      createFile(logger, context, NoDeployResourcesShim.PREFIX + "inGenerated.txt");
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unable to create test file", e);
+      throw new UnableToCompleteException();
+    }
+
+    return typeName;
+  }
+
+  private void createFile(TreeLogger logger, GeneratorContext context,
+      String path) throws UnableToCompleteException, IOException {
+
+    OutputStream out = context.tryCreateResource(logger, path);
+    if (out == null) {
+      return;
+    }
+
+    out.write(Util.getBytes(NoDeployTest.TEST_TEXT));
+    context.commitResource(logger, out);
+  }
+}