Changes to support experimental development mode work:
- Adds a hook to CrossSiteIframeLinker that a bookmarklet can use to change the URL loaded for a module.
- Introduces the ResourceLoader interface, which allows the compiler's classpath to be modified without having to create a ClassLoader.
- Adds clearCache() methods to force the compiler to reload files when compiling the same code a second time.
- Adds javadoc for flag processing code.

Review at http://gwt-code-reviews.appspot.com/1594803


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10767 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
index 1c74cf1..db74092 100644
--- a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeLinker.java
@@ -95,6 +95,7 @@
     includeJs(ss, logger, getJsComputeUrlForResource(context), "__COMPUTE_URL_FOR_RESOURCE__");
     includeJs(ss, logger, getJsLoadExternalStylesheets(context), "__LOAD_STYLESHEETS__");
     includeJs(ss, logger, getJsRunAsync(context), "__RUN_ASYNC__");
+    includeJs(ss, logger, getJsDevModeRedirectHook(context), "__DEV_MODE_REDIRECT_HOOK__");
 
     // This Linker does not support <script> tags in the gwt.xml
     SortedSet<ScriptReference> scripts = artifacts.find(ScriptReference.class);
@@ -208,6 +209,15 @@
   }
 
   /**
+   * Returns a code fragment to check for the new development mode.
+   */
+  protected String getJsDevModeRedirectHook(LinkerContext context) {
+    // Temporarily disabled by default.
+    // return "com/google/gwt/core/linker/DevModeRedirectHook.js";
+    return "";
+  }
+
+  /**
    * Returns the name of the {@code JsInstallLocation} script.  By default,
    * returns {@code "com/google/gwt/core/ext/linker/impl/installLocationIframe.js"}.
    *
@@ -513,10 +523,15 @@
   @Override
   protected String wrapPrimaryFragment(TreeLogger logger, LinkerContext context, String script,
       ArtifactSet artifacts, CompilationResult result) throws UnableToCompleteException {
-    StringBuffer out = new StringBuffer();
+
+    StringBuilder out = new StringBuilder();
+
     if (shouldIncludeBootstrapInPrimaryFragment(context)) {
       out.append(generateSelectionScript(logger, context, artifacts, result));
     }
+
+    out.append("if (" + context.getModuleFunctionName() + ".succeeded) {\n");
+
     if (shouldInstallCode(context)) {
       // Rewrite the code so it can be installed with
       // __MODULE_FUNC__.onScriptDownloaded
@@ -528,10 +543,14 @@
         newChunks.add(JsToStringGenerationVisitor.javaScriptString(chunk));
       }
       out.append(Joiner.on(", ").join(newChunks));
-      out.append("])");
+      out.append("]);\n");
     } else {
       out.append(script);
+      out.append("\n");
     }
+
+    out.append("}\n");
+
     return out.toString();
   }
 }
diff --git a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeTemplate.js b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeTemplate.js
index 5798f0f..179f00e 100644
--- a/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeTemplate.js
+++ b/dev/core/src/com/google/gwt/core/linker/CrossSiteIframeTemplate.js
@@ -17,6 +17,9 @@
 function __MODULE_FUNC__() {
   var $wnd = __WINDOW_DEF__;
   var $doc = __DOCUMENT_DEF__;
+
+  __DEV_MODE_REDIRECT_HOOK__
+
   sendStats('bootstrap', 'begin');
 
   /****************************************************************************
@@ -127,8 +130,9 @@
 
   installScript(filename);
 
+  return true; // success
 }
-__MODULE_FUNC__();
+__MODULE_FUNC__.succeeded = __MODULE_FUNC__();
 
 __END_TRY_BLOCK_AND_START_CATCH__
   __MODULE_FUNC_ERROR_CATCH__
diff --git a/dev/core/src/com/google/gwt/core/linker/DevModeRedirectHook.js b/dev/core/src/com/google/gwt/core/linker/DevModeRedirectHook.js
new file mode 100644
index 0000000..6a04155
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/linker/DevModeRedirectHook.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2011 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.
+ */
+
+// A snippet of code that loads a different script if dev mode is enabled.
+
+// We use a different key for each module so that we can turn on dev mode
+// independently for each.
+var devModeKey = '__gwtDevModeHook:__MODULE_NAME__';
+
+// If dev mode is on, the Bookmarklet previously saved the code server's URL
+// to session storage.
+var devModeUrl = $wnd.sessionStorage[devModeKey];
+
+if (devModeUrl && !$wnd[devModeKey]) {
+  $wnd[devModeKey] = true; // Don't try to redirect more than once,
+  var script = $doc.createElement('script');
+  script.src = devModeUrl;
+  var head = $doc.getElementsByTagName('head')[0];
+
+  // The new script tag must come before the previous one so that
+  // computeScriptBase will see it.
+  head.insertBefore(script, head.firstElementChild);
+
+  return false; // Skip the regular bootstrap.
+}
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 3b0b812..8d972e9 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
@@ -139,6 +139,7 @@
   private final long moduleDefCreationTime = System.currentTimeMillis();
 
   private final String name;
+  private final ResourceLoader resources;
 
   /**
    * Must use a separate field to track override, because setNameOverride() will
@@ -163,7 +164,12 @@
   private final Styles styles = new Styles();
 
   public ModuleDef(String name) {
+    this(name, ResourceLoaders.forClassLoader(Thread.currentThread()));
+  }
+
+  public ModuleDef(String name, ResourceLoader resources) {
     this.name = name;
+    this.resources = resources;
     defaultFilters = new DefaultFilters();
   }
 
@@ -401,7 +407,7 @@
 
   public synchronized ResourceOracle getResourcesOracle() {
     if (lazyResourcesOracle == null) {
-      lazyResourcesOracle = new ResourceOracleImpl(TreeLogger.NULL);
+      lazyResourcesOracle = new ResourceOracleImpl(TreeLogger.NULL, resources);
       PathPrefixSet pathPrefixes = lazySourceOracle.getPathPrefixes();
       PathPrefixSet newPathPrefixes = new PathPrefixSet();
       for (PathPrefix pathPrefix : pathPrefixes.values()) {
@@ -536,12 +542,12 @@
 
     // Create the public path.
     TreeLogger branch = Messages.PUBLIC_PATH_LOCATIONS.branch(logger, null);
-    lazyPublicOracle = new ResourceOracleImpl(branch);
+    lazyPublicOracle = new ResourceOracleImpl(branch, resources);
     lazyPublicOracle.setPathPrefixes(publicPrefixSet);
 
     // Create the source path.
     branch = Messages.SOURCE_PATH_LOCATIONS.branch(logger, null);
-    lazySourceOracle = new ResourceOracleImpl(branch);
+    lazySourceOracle = new ResourceOracleImpl(branch, resources);
     lazySourceOracle.setPathPrefixes(sourcePrefixSet);
 
     needsRefresh = true;
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefLoader.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefLoader.java
index 94a1f7f..f7021d3 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefLoader.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefLoader.java
@@ -37,28 +37,12 @@
 /**
  * The top-level API for loading module XML.
  */
-public final class ModuleDefLoader {
+public class ModuleDefLoader {
   /*
    * TODO(scottb,tobyr,zundel): synchronization????
    */
 
   /**
-   * Interface to provide a load strategy to the load process.
-   */
-  private interface LoadStrategy {
-    /**
-     * Perform loading on the specified module.
-     *
-     * @param logger logs the process
-     * @param moduleName the name of the process
-     * @param moduleDef a module
-     * @throws UnableToCompleteException
-     */
-    void load(TreeLogger logger, String moduleName, ModuleDef moduleDef)
-        throws UnableToCompleteException;
-  }
-
-  /**
    * Filename suffix used for GWT Module XML files.
    */
   public static final String GWT_MODULE_XML_SUFFIX = ".gwt.xml";
@@ -83,6 +67,10 @@
   private static final Map<String, String> moduleEffectiveNameToPhysicalName =
     new HashMap<String, String>();
 
+  public static void clearModuleCache() {
+    getModulesCache().clear();
+  }
+
   /**
    * Creates a module in memory that is not associated with a
    * <code>.gwt.xml</code> file on disk.
@@ -95,14 +83,27 @@
    * @throws UnableToCompleteException
    */
   public static ModuleDef createSyntheticModule(TreeLogger logger,
-      String moduleName, String[] inherits, boolean refresh)
+      String moduleName, final String[] inherits, boolean refresh)
       throws UnableToCompleteException {
     ModuleDef moduleDef = tryGetLoadedModule(moduleName, refresh);
     if (moduleDef != null) {
       return moduleDef;
     }
-    ModuleDefLoader loader = new ModuleDefLoader(inherits);
-    ModuleDef module = loader.doLoadModule(logger, moduleName);
+
+    ResourceLoader resources = ResourceLoaders.forClassLoader(Thread.currentThread());
+
+    ModuleDefLoader loader = new ModuleDefLoader(resources) {
+      @Override
+      protected void load(TreeLogger logger, String nameOfModuleToLoad, ModuleDef dest)
+          throws UnableToCompleteException {
+        logger.log(TreeLogger.TRACE, "Loading module '" + nameOfModuleToLoad + "'");
+        for (String inherit : inherits) {
+          nestedLoad(logger, inherit, dest);
+        }
+      }
+    };
+
+    ModuleDef module = doLoadModule(loader, logger, moduleName, resources);
     /*
      * Must reset name override on synthetic modules. Otherwise they'll be
      * incorrectly affected by the last inherits tag, because they have no XML
@@ -137,6 +138,23 @@
    */
   public static ModuleDef loadFromClassPath(TreeLogger logger,
       String moduleName, boolean refresh) throws UnableToCompleteException {
+
+    ResourceLoader resources = ResourceLoaders.forClassLoader(Thread.currentThread());
+
+    return loadFromResources(logger, moduleName, resources, refresh);
+  }
+
+  /**
+   * Loads a new module from the given ResourceLoader.
+   * @param moduleName the module to load
+   * @param resources where to look for module.xml and module.gwtar files.
+   * @param refresh whether to refresh the module
+   * @return the loaded module
+   * @throws UnableToCompleteException
+   */
+  public static ModuleDef loadFromResources(TreeLogger logger, String moduleName,
+      ResourceLoader resources, boolean refresh) throws UnableToCompleteException {
+
     Event moduleDefLoadFromClassPathEvent = SpeedTracerLogger.start(
         CompilerEventType.MODULE_DEF, "phase", "loadFromClassPath", "moduleName", moduleName);
     try {
@@ -150,13 +168,48 @@
       if (moduleDef != null) {
         return moduleDef;
       }
-      ModuleDefLoader loader = new ModuleDefLoader();
-      return loader.doLoadModule(logger, moduleName);
+      ModuleDefLoader loader = new ModuleDefLoader(resources);
+      return ModuleDefLoader.doLoadModule(loader, logger, moduleName, resources);
     } finally {
       moduleDefLoadFromClassPathEvent.end();
     }
   }
 
+  /**
+   * This method loads a module.
+   *
+   * @param loader the loader to use
+   * @param logger used to log the loading process
+   * @param moduleName the name of the module
+   * @param resources where to load source code from
+   * @return the module returned -- cannot be null
+   * @throws UnableToCompleteException if module loading failed
+   */
+  private static ModuleDef doLoadModule(ModuleDefLoader loader, TreeLogger logger,
+      String moduleName, ResourceLoader resources)
+      throws UnableToCompleteException {
+
+    ModuleDef moduleDef = new ModuleDef(moduleName, resources);
+    Event moduleLoadEvent = SpeedTracerLogger.start(CompilerEventType.MODULE_DEF,
+        "phase", "strategy.load()");
+    loader.load(logger, moduleName, moduleDef);
+    moduleLoadEvent.end();
+
+    // Do any final setup.
+    //
+    Event moduleNormalizeEvent = SpeedTracerLogger.start(CompilerEventType.MODULE_DEF,
+        "phase", "moduleDef.normalize()");
+    moduleDef.normalize(logger);
+    moduleNormalizeEvent.end();
+
+    // Add the "physical" module name: com.google.Module
+    getModulesCache().put(moduleName, moduleDef);
+
+    // Add a mapping from the module's effective name to its physical name
+    moduleEffectiveNameToPhysicalName.put(moduleDef.getName(), moduleName);
+    return moduleDef;
+  }
+
   @SuppressWarnings("unchecked")
   private static Map<String, ModuleDef> getModulesCache() {
     ClassLoader keyClassLoader = Thread.currentThread().getContextClassLoader();
@@ -178,45 +231,24 @@
     return moduleDef;
   }
 
-  private final ClassLoader classLoader;
+  private final ResourceLoader resourceLoader;
 
-  private final LoadStrategy strategy;
-
-  /**
-   * Constructs a {@link ModuleDefLoader} that loads from the class path.
-   */
-  private ModuleDefLoader() {
-    this.classLoader = Thread.currentThread().getContextClassLoader();
-    this.strategy = new LoadStrategy() {
-      @Override
-      public void load(TreeLogger logger, String moduleName, ModuleDef moduleDef)
-          throws UnableToCompleteException {
-        nestedLoad(logger, moduleName, moduleDef);
-      }
-    };
+  private ModuleDefLoader(ResourceLoader loader) {
+    this.resourceLoader = loader;
   }
 
   /**
-   * Constructs a {@link ModuleDefLoader} that loads a synthetic module.
-   *
-   * @param inherits a set of modules to inherit from
+   * Loads a module and all its included modules, recursively, into the given ModuleDef.
+   * @throws UnableToCompleteException
    */
-  private ModuleDefLoader(final String[] inherits) {
-    this.classLoader = Thread.currentThread().getContextClassLoader();
-    this.strategy = new LoadStrategy() {
-      @Override
-      public void load(TreeLogger logger, String moduleName, ModuleDef moduleDef)
-          throws UnableToCompleteException {
-        logger.log(TreeLogger.TRACE, "Loading module '" + moduleName + "'");
-        for (String inherit : inherits) {
-          nestedLoad(logger, inherit, moduleDef);
-        }
-      }
-    };
+  protected void load(TreeLogger logger, String nameOfModuleToLoad, ModuleDef dest)
+      throws UnableToCompleteException {
+    nestedLoad(logger, nameOfModuleToLoad, dest);
   }
 
   /**
-   * Loads a new module into <code>moduleDef</code> as an included module.
+   * Loads a new module and its descendants into <code>moduleDef</code> as included modules.
+   * (If there are any descendants, this method will be called recursively.)
    *
    * @param parentLogger Logs the process.
    * @param moduleName The module to load.
@@ -245,7 +277,7 @@
     //
     String slashedModuleName = moduleName.replace('.', '/');
     String resName = slashedModuleName + ModuleDefLoader.GWT_MODULE_XML_SUFFIX;
-    URL moduleURL = classLoader.getResource(resName);
+    URL moduleURL = resourceLoader.getResource(resName);
 
     if (moduleURL != null) {
       String externalForm = moduleURL.toExternalForm();
@@ -265,7 +297,7 @@
         throw new UnableToCompleteException();
       }
       String compilationUnitArchiveName = slashedModuleName + ModuleDefLoader.COMPILATION_UNIT_ARCHIVE_SUFFIX;
-      URL compiledModuleURL = classLoader.getResource(compilationUnitArchiveName);
+      URL compiledModuleURL = resourceLoader.getResource(compilationUnitArchiveName);
       if (compiledModuleURL != null) {
         moduleDef.addCompilationUnitArchiveURL(compiledModuleURL);
       }
@@ -299,36 +331,4 @@
       Utility.close(r);
     }
   }
-
-  /**
-   * This method loads a module.
-   *
-   * @param logger used to log the loading process
-   * @param moduleName the name of the module
-   * @return the module returned -- cannot be null
-   * @throws UnableToCompleteException if module loading failed
-   */
-  private ModuleDef doLoadModule(TreeLogger logger, String moduleName)
-      throws UnableToCompleteException {
-
-    ModuleDef moduleDef = new ModuleDef(moduleName);
-    Event moduleLoadEvent = SpeedTracerLogger.start(CompilerEventType.MODULE_DEF,
-        "phase", "strategy.load()");
-    strategy.load(logger, moduleName, moduleDef);
-    moduleLoadEvent.end();
-
-    // Do any final setup.
-    //
-    Event moduleNormalizeEvent = SpeedTracerLogger.start(CompilerEventType.MODULE_DEF,
-        "phase", "moduleDef.normalize()");
-    moduleDef.normalize(logger);
-    moduleNormalizeEvent.end();
-
-    // Add the "physical" module name: com.google.Module
-    getModulesCache().put(moduleName, moduleDef);
-
-    // Add a mapping from the module's effective name to its physical name
-    moduleEffectiveNameToPhysicalName.put(moduleDef.getName(), moduleName);
-    return moduleDef;
-  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ResourceLoader.java b/dev/core/src/com/google/gwt/dev/cfg/ResourceLoader.java
new file mode 100644
index 0000000..c7e92c4
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/cfg/ResourceLoader.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2011 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.cfg;
+
+import java.net.URL;
+import java.util.List;
+
+/**
+ * A classpath-like way of loading files.
+ * (Must implement equals and hashCode to work as a key in a HashMap.)
+ */
+public interface ResourceLoader {
+
+  /**
+   * Returns the URLs that will be searched in order for files.
+   */
+  List<URL> getClassPath();
+
+  /**
+   * Returns a URL that may be used to load the resource, or null if the
+   * resource can't be found.
+   *
+   * <p> (The API is the same as {@link ClassLoader#getResource(String)}.) </p>
+   */
+  URL getResource(String resourceName);
+}
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ResourceLoaders.java b/dev/core/src/com/google/gwt/dev/cfg/ResourceLoaders.java
new file mode 100644
index 0000000..e97e09e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/cfg/ResourceLoaders.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2011 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.cfg;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Creates instances of {@link ResourceLoader}.
+ */
+public class ResourceLoaders {
+
+  private static class ClassLoaderAdapter implements ResourceLoader {
+    private final ClassLoader wrapped;
+
+    public ClassLoaderAdapter(ClassLoader wrapped) {
+      this.wrapped = wrapped;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof ClassLoaderAdapter)) {
+        return false;
+      }
+      ClassLoaderAdapter otherAdapter = (ClassLoaderAdapter) other;
+      return wrapped.equals(otherAdapter.wrapped);
+    }
+
+    /**
+     * Returns the URLs of the wrapped ClassLoader and each parent that's a URLClassLoader.
+     */
+    @Override
+    public List<URL> getClassPath() {
+      List<URL> result = new ArrayList<URL>();
+      for (ClassLoader candidate = wrapped; candidate != null; candidate = candidate.getParent()) {
+        if (candidate instanceof URLClassLoader) {
+          URL[] urls = ((URLClassLoader) candidate).getURLs();
+          result.addAll(Arrays.asList(urls));
+        }
+      }
+      return result;
+    }
+
+    @Override
+    public URL getResource(String resourceName) {
+      return wrapped.getResource(resourceName);
+    }
+
+    @Override
+    public int hashCode() {
+      return wrapped.hashCode();
+    }
+  }
+
+  /**
+   * A ResourceLoader that prefixes some directories to another ResourceLoader.
+   */
+  private static class PrefixLoader implements ResourceLoader {
+    private final List<File> path;
+    private final List<URL> pathAsUrls = new ArrayList<URL>();
+    private final ResourceLoader fallback;
+
+    public PrefixLoader(List<File> path, ResourceLoader fallback) {
+      assert path != null;
+      this.path = path;
+      this.fallback = fallback;
+      for (File file : path) {
+        try {
+          pathAsUrls.add(file.toURI().toURL());
+        } catch (MalformedURLException e) {
+          throw new RuntimeException("can't create URL for file: " + file);
+        }
+      }
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof PrefixLoader)) {
+        return false;
+      }
+      PrefixLoader otherLoader = (PrefixLoader) other;
+      return path.equals(otherLoader.path) && fallback.equals(otherLoader.fallback);
+    }
+
+    @Override
+    public List<URL> getClassPath() {
+      List<URL> result = new ArrayList<URL>();
+      result.addAll(pathAsUrls);
+      result.addAll(fallback.getClassPath());
+      return result;
+    }
+
+    @Override
+    public URL getResource(String resourceName) {
+      for (File prefix : path) {
+        File candidate = new File(prefix, resourceName);
+        if (candidate.exists()) {
+          try {
+            return candidate.toURI().toURL();
+          } catch (MalformedURLException e) {
+            return null;
+          }
+        }
+      }
+      return fallback.getResource(resourceName);
+    }
+
+    @Override
+    public int hashCode() {
+      return path.hashCode() ^ fallback.hashCode();
+    }
+  }
+
+  /**
+   * Creates a ResourceLoader that loads from the given thread's class loader.
+   */
+  public static ResourceLoader forClassLoader(Thread thread) {
+    return wrap(thread.getContextClassLoader());
+  }
+
+  /**
+   * Creates a ResourceLoader that loads from a list of directories and falls back
+   * to another ResourceLoader.
+   */
+  public static ResourceLoader forPathAndFallback(List<File> path, ResourceLoader fallback) {
+    return new PrefixLoader(path, fallback);
+  }
+  
+  /**
+   * Adapts a ClassLoader to work as a ResourceLoader.
+   * (Caveat: any ClassLoader in the chain that isn't a URLClassLoader won't contribute to the
+   * results of {@link  ResourceLoader#getClassPath}.)
+   */
+  public static ResourceLoader wrap(ClassLoader loader) {
+    return new ClassLoaderAdapter(loader);
+  }
+
+  private ResourceLoaders() {
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ResourceOracleImpl.java b/dev/core/src/com/google/gwt/dev/resource/impl/ResourceOracleImpl.java
index 9c2aa78..5d598e1 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/ResourceOracleImpl.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/ResourceOracleImpl.java
@@ -16,6 +16,8 @@
 package com.google.gwt.dev.resource.impl;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.cfg.ResourceLoader;
+import com.google.gwt.dev.cfg.ResourceLoaders;
 import com.google.gwt.dev.resource.Resource;
 import com.google.gwt.dev.resource.ResourceOracle;
 import com.google.gwt.dev.util.log.speedtracer.CompilerEventType;
@@ -33,7 +35,6 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.net.URLClassLoader;
 import java.security.AccessControlException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -153,8 +154,12 @@
   }
 
   @SuppressWarnings("unchecked")
-  private static final Map<ClassLoader, List<ClassPathEntry>> classPathCache = new ReferenceMap(
+  private static final Map<ResourceLoader, List<ClassPathEntry>> classPathCache = new ReferenceMap(
       AbstractReferenceMap.WEAK, AbstractReferenceMap.HARD);
+  
+  public static void clearCache() {
+    classPathCache.clear();
+  }
 
   public static ClassPathEntry createEntryForUrl(TreeLogger logger, URL url)
       throws URISyntaxException, IOException {
@@ -197,9 +202,16 @@
    * Preinitializes the classpath for a given {@link ClassLoader}.
    */
   public static void preload(TreeLogger logger, ClassLoader classLoader) {
+    preload(logger, ResourceLoaders.wrap(classLoader));
+  }
+
+  /**
+   * Preinitializes the classpath for a given {@link ResourceLoader}.
+   */
+  public static void preload(TreeLogger logger, ResourceLoader resources) {
     Event resourceOracle =
         SpeedTracerLogger.start(CompilerEventType.RESOURCE_ORACLE, "phase", "preload");
-    List<ClassPathEntry> entries = getAllClassPathEntries(logger, classLoader);
+    List<ClassPathEntry> entries = getAllClassPathEntries(logger, resources);
     for (ClassPathEntry entry : entries) {
       // We only handle pre-indexing jars, the file system could change.
       if (entry instanceof ZipFileClassPathEntry) {
@@ -301,58 +313,53 @@
     resourceOracle.end();
   }
 
-  private static void addAllClassPathEntries(TreeLogger logger, ClassLoader classLoader,
+  private static void addAllClassPathEntries(TreeLogger logger, ResourceLoader loader,
       List<ClassPathEntry> classPath) {
     // URL is expensive in collections, so we use URI instead
     // See:
     // http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html
     Set<URI> seenEntries = new HashSet<URI>();
-    for (; classLoader != null; classLoader = classLoader.getParent()) {
-      if (classLoader instanceof URLClassLoader) {
-        URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
-        URL[] urls = urlClassLoader.getURLs();
-        for (URL url : urls) {
-          URI uri;
-          try {
-            uri = url.toURI();
-          } catch (URISyntaxException e) {
-            logger.log(TreeLogger.WARN, "Error processing classpath URL '" + url + "'", e);
-            continue;
-          }
-          if (seenEntries.contains(uri)) {
-            continue;
-          }
-          seenEntries.add(uri);
-          Throwable caught;
-          try {
-            ClassPathEntry entry = createEntryForUrl(logger, url);
-            if (entry != null) {
-              classPath.add(entry);
-            }
-            continue;
-          } catch (AccessControlException e) {
-            if (logger.isLoggable(TreeLogger.DEBUG)) {
-              logger.log(TreeLogger.DEBUG, "Skipping URL due to access restrictions: " + url);
-            }
-            continue;
-          } catch (URISyntaxException e) {
-            caught = e;
-          } catch (IOException e) {
-            caught = e;
-          }
-          logger.log(TreeLogger.WARN, "Error processing classpath URL '" + url + "'", caught);
-        }
+
+    for (URL url : loader.getClassPath()) {
+      URI uri;
+      try {
+        uri = url.toURI();
+      } catch (URISyntaxException e) {
+        logger.log(TreeLogger.WARN, "Error processing classpath URL '" + url + "'", e);
+        continue;
       }
+      if (seenEntries.contains(uri)) {
+        continue;
+      }
+      seenEntries.add(uri);
+      Throwable caught;
+      try {
+        ClassPathEntry entry = createEntryForUrl(logger, url);
+        if (entry != null) {
+          classPath.add(entry);
+        }
+        continue;
+      } catch (AccessControlException e) {
+        if (logger.isLoggable(TreeLogger.DEBUG)) {
+          logger.log(TreeLogger.DEBUG, "Skipping URL due to access restrictions: " + url);
+        }
+        continue;
+      } catch (URISyntaxException e) {
+        caught = e;
+      } catch (IOException e) {
+        caught = e;
+      }
+      logger.log(TreeLogger.WARN, "Error processing classpath URL '" + url + "'", caught);
     }
   }
 
   private static synchronized List<ClassPathEntry> getAllClassPathEntries(TreeLogger logger,
-      ClassLoader classLoader) {
-    List<ClassPathEntry> classPath = classPathCache.get(classLoader);
+      ResourceLoader resources) {
+    List<ClassPathEntry> classPath = classPathCache.get(resources);
     if (classPath == null) {
       classPath = new ArrayList<ClassPathEntry>();
-      addAllClassPathEntries(logger, classLoader, classPath);
-      classPathCache.put(classLoader, classPath);
+      addAllClassPathEntries(logger, resources, classPath);
+      classPathCache.put(resources, classPath);
     }
     return classPath;
   }
@@ -387,11 +394,15 @@
   /**
    * Constructs a {@link ResourceOracleImpl} from a {@link ClassLoader}. The
    * specified {@link ClassLoader} and all of its parents which are instances of
-   * {@link URLClassLoader} will have their class path entries added to this
+   * {@link java.net.URLClassLoader} will have their class path entries added to this
    * instances underlying class path.
    */
   public ResourceOracleImpl(TreeLogger logger, ClassLoader classLoader) {
-    this(getAllClassPathEntries(logger, classLoader));
+    this(logger, ResourceLoaders.wrap(classLoader));
+  }
+
+  public ResourceOracleImpl(TreeLogger logger, ResourceLoader resources) {
+    this(getAllClassPathEntries(logger, resources));
   }
 
   @Override
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ZipFileClassPathEntry.java b/dev/core/src/com/google/gwt/dev/resource/impl/ZipFileClassPathEntry.java
index af62c83..17a4144 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/ZipFileClassPathEntry.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/ZipFileClassPathEntry.java
@@ -79,6 +79,10 @@
   private static final Map<String, ZipFileClassPathEntry> entryCache = new ReferenceMap(
       AbstractReferenceMap.HARD, AbstractReferenceMap.SOFT);
 
+  public static void clearCache() {
+    entryCache.clear();
+  }
+
   /**
    * @return the {@link ZipFileClassPathEntry} instance for given jar or zip
    *         file, may be shared with other users.
diff --git a/dev/core/src/com/google/gwt/util/tools/ArgHandler.java b/dev/core/src/com/google/gwt/util/tools/ArgHandler.java
index 81f7ea1..d0124bd 100644
--- a/dev/core/src/com/google/gwt/util/tools/ArgHandler.java
+++ b/dev/core/src/com/google/gwt/util/tools/ArgHandler.java
@@ -28,8 +28,22 @@
 
   public abstract String getTag();
 
+  /**
+   * A list of words representing the arguments in help text.
+   */
   public abstract String[] getTagArgs();
 
+  /**
+   * Attempts to process one flag or "extra" command-line argument (that appears
+   * without a flag).
+   * @param args  the arguments passed in to main()
+   * @param tagIndex  an index into args indicating the first argument to use.
+   * If this is a handler for a flag argument. Otherwise it's the index of the
+   * "extra" argument.
+   * @return the number of additional arguments consumed, not including the flag or
+   * extra argument. Alternately, returns -1 if the argument cannot be used. This will
+   * causes the program to abort and usage to be displayed.
+   */
   public abstract int handle(String[] args, int tagIndex);
 
   public boolean isRequired() {
diff --git a/dev/core/src/com/google/gwt/util/tools/ArgHandlerExtra.java b/dev/core/src/com/google/gwt/util/tools/ArgHandlerExtra.java
index eaca29d..6335cb7 100644
--- a/dev/core/src/com/google/gwt/util/tools/ArgHandlerExtra.java
+++ b/dev/core/src/com/google/gwt/util/tools/ArgHandlerExtra.java
@@ -20,6 +20,10 @@
  */
 public abstract class ArgHandlerExtra extends ArgHandler {
 
+  /**
+   * Processes the given "extra" argument.
+   * @return false to abort the command and print a usage error.
+   */
   public abstract boolean addExtraArg(String arg);
 
   @Override