This patch updates the ResourceOracleImpl to use a PathPrefix order and prefer PathPrefix order 
over Classpath order. It also adds/updates tests to confirm that the above rules are enforced.

Patch by: amitmanjhi
Review by: jat, scottb



git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4520 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ClassPathEntry.java b/dev/core/src/com/google/gwt/dev/resource/impl/ClassPathEntry.java
index aaa46f9..5ada435 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/ClassPathEntry.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/ClassPathEntry.java
@@ -17,7 +17,7 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 
-import java.util.Set;
+import java.util.Map;
 
 /**
  * A location that acts as a starting point for finding resources
@@ -30,11 +30,12 @@
    * begins with a prefix X from the path prefix set and P is allowed by the
    * filter associated with X.
    * 
-   * @return a set of zero or more resources; note no guarantees are made
-   *         regarding the identities of the returned resource objects, and the
-   *         same object may be returned across multiple calls
+   * @return a map with key as an allowed resource and value as the PathPrefix
+   *         that allows the resource; note no guarantees are made regarding the
+   *         identities of the returned resource objects, and the same object
+   *         may be returned across multiple calls
    */
-  public abstract Set<AbstractResource> findApplicableResources(
+  public abstract Map<AbstractResource, PathPrefix> findApplicableResources(
       TreeLogger logger, PathPrefixSet pathPrefixSet);
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java b/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java
index 7d29ca6..2fd2964 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java
@@ -19,8 +19,8 @@
 import com.google.gwt.dev.util.msg.Message1String;
 
 import java.io.File;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.IdentityHashMap;
+import java.util.Map;
 
 /**
  * TODO(bruce): write me.
@@ -48,9 +48,9 @@
   }
 
   @Override
-  public Set<AbstractResource> findApplicableResources(TreeLogger logger,
-      PathPrefixSet pathPrefixSet) {
-    Set<AbstractResource> results = new HashSet<AbstractResource>();
+  public Map<AbstractResource, PathPrefix> findApplicableResources(
+      TreeLogger logger, PathPrefixSet pathPrefixSet) {
+    Map<AbstractResource, PathPrefix> results = new IdentityHashMap<AbstractResource, PathPrefix>();
     descendToFindResources(logger, pathPrefixSet, results, dir, "");
     return results;
   }
@@ -62,14 +62,15 @@
 
   /**
    * @param logger logs progress
-   * @param resources the accumulating set of resources found
+   * @param resources the accumulating set of resources (each with the
+   *          corresponding pathPrefix) found
    * @param dir the file or directory to consider
    * @param dirPath the abstract path name associated with 'parent', which
    *          explicitly does not include the classpath entry in its path
    */
   private void descendToFindResources(TreeLogger logger,
-      PathPrefixSet pathPrefixSet, Set<AbstractResource> resources, File dir,
-      String dirPath) {
+      PathPrefixSet pathPrefixSet, Map<AbstractResource, PathPrefix> resources,
+      File dir, String dirPath) {
     assert (dir.isDirectory());
 
     // Assert: this directory is included in the path prefix set.
@@ -89,10 +90,11 @@
               null);
         }
       } else {
-        if (pathPrefixSet.includesResource(childPath)) {
+        PathPrefix prefix = null;
+        if ((prefix = pathPrefixSet.includesResource(childPath)) != null) {
           Messages.INCLUDING_FILE.log(logger, childPath, null);
           FileResource r = new FileResource(this, childPath, child);
-          resources.add(r);
+          resources.put(r, prefix);
         } else {
           Messages.EXCLUDING_FILE.log(logger, childPath, null);
         }
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java b/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java
index 0e72bf4..c9c5b2d 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java
@@ -15,23 +15,33 @@
  */
 package com.google.gwt.dev.resource.impl;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /**
  * Combines the information conveyed about a set of path prefixes to quickly
  * answer questions regarding an entire set of path prefixes.
+ * 
  */
 public class PathPrefixSet {
+  /*
+   * (1) TODO(amitmanjhi): Support multiple PathPrefixes with different filters
+   * but the same "path". This could arise when client code inherits some
+   * library code and defines the same path prefix with a more specific resource
+   * filter.
+   * 
+   * (2) TODO(amitmanjhi): Improve the api of the PathPrefixSet so that with one
+   * trie-traversal, it could be found out which resources rooted at a directory
+   * are allowed? 
+   */
 
   private static class TrieNode {
-    // TODO(bruce): test to see if Map would be faster; I'm on the fence
-    private final List<TrieNode> children = new ArrayList<TrieNode>();
+    // TODO(amitmanjhi): Consider the memory-speed tradeoff here
+    private final Map<String, TrieNode> children = new HashMap<String, TrieNode>();
     private final String part;
+
     private PathPrefix prefix;
 
     public TrieNode(String part) {
@@ -39,19 +49,14 @@
     }
 
     public TrieNode addChild(String part) {
-      assert (findChild(part) == null);
       TrieNode newChild = new TrieNode(part);
-      children.add(newChild);
+      TrieNode oldChild = children.put(part, newChild);
+      assert (oldChild == null);
       return newChild;
     }
 
     public TrieNode findChild(String part) {
-      for (TrieNode child : children) {
-        if (child.part.equals(part)) {
-          return child;
-        }
-      }
-      return null;
+      return children.get(part);
     }
 
     public PathPrefix getPathPrefix() {
@@ -80,14 +85,21 @@
       sb.append(indent);
       sb.append(' ');
       sb.append(part);
-      for (TrieNode child : children) {
+      for (TrieNode child : children.values()) {
         child.toString(sb, indent + "  ");
       }
     }
   }
 
-  private int modCount;
-  private final Map<String, PathPrefix> prefixes = new HashMap<String, PathPrefix>();
+  /**
+   * The sequence number in which the PathPrefix was added.
+   */
+  private int size = 0;
+
+  /**
+   * Map of pathPrefix => the sequence number when the pathPrefix was added.
+   */
+  private final Map<PathPrefix, Integer> prefixes = new HashMap<PathPrefix, Integer>();
   private final TrieNode rootTrieNode = new TrieNode("/");
 
   /**
@@ -98,9 +110,9 @@
    *         wins)
    */
   public boolean add(PathPrefix prefix) {
-    ++modCount;
+    ++size;
+    prefixes.put(prefix, size);
     String pathPrefix = prefix.getPrefix();
-    prefixes.put(pathPrefix, prefix);
 
     /*
      * An empty prefix means we have no prefix requirement, but we do attached
@@ -132,8 +144,8 @@
     return didAdd;
   }
 
-  public int getModCount() {
-    return modCount;
+  public int getSize() {
+    return size;
   }
 
   /**
@@ -141,19 +153,17 @@
    * included. The primary purpose of this method is to allow
    * {@link ClassPathEntry} subclasses to avoid descending into directory
    * hierarchies that could not possibly contain resources that would be
-   * included by {@link #includesResource(String).
+   * included by {@link #includesResource(String)}
    * 
-   * @param dirPath must be a valid abstract directory name or the empty string
-   * @return
+   * @param dirPath must be a valid abstract directory name (must not be an
+   *          empty string)
+   * @return true if some PathPrefix allows the directory
    */
   public boolean includesDirectory(String dirPath) {
     assertValidAbstractDirectoryPathName(dirPath);
 
     /*
-     * There are five cases:
-     * 
-     * (0) dirPath is the empty string, which is (a) trivially included unless
-     * (b) no prefix paths have been specified at all.
+     * There are four cases:
      * 
      * (1) The empty string was specified as a prefix, which causes everything
      * to be included.
@@ -171,22 +181,12 @@
      * includes it).
      */
 
-    // if ("".equals(dirPath)) {
-    // if (rootTrieNode.hasChildren() || rootTrieNode.getPathPrefix() != null) {
-    // // Case (0)(a): trivially true.
-    // return true;
-    // } else {
-    // // Case (0)(b): no directories are included.
-    // return false;
-    // }
-    // }
     if (rootTrieNode.getPathPrefix() != null) {
       // Case (1).
       return true;
     }
 
     TrieNode parentNode = rootTrieNode;
-
     String[] parts = dirPath.split("/");
     for (String part : parts) {
       assert (!"".equals(part));
@@ -215,10 +215,12 @@
    * prefix set and the corresponding filters.
    * 
    * @param resourceAbstractPathName
-   * @return <code>true</code> if the resource matches some specified prefix
-   *         and any associated filters don't exclude it
+   * @return matching <code>PathPrefix</code> if the resource matches some
+   *         specified prefix and any associated filters don't exclude it.
+   *         Otherwise, returns null. So it returns null if either no prefixes
+   *         match or the most specific prefix excludes the resource.
    */
-  public boolean includesResource(String resourceAbstractPathName) {
+  public PathPrefix includesResource(String resourceAbstractPathName) {
     /*
      * Algorithm: dive down the package hierarchy looking for the most specific
      * package that applies to this resource. The filter of the most specific
@@ -232,12 +234,8 @@
     TrieNode currentNode = rootTrieNode;
     PathPrefix mostSpecificPrefix = rootTrieNode.getPathPrefix();
 
-    // TODO(bruce): consider not using split for speed
-    String[] parts = resourceAbstractPathName.split("/");
-
     // Walk all but the last path part, which is assumed to be a file name.
-    for (int i = 0, n = parts.length - 1; i < n; ++i) {
-      String part = parts[i];
+    for (String part : resourceAbstractPathName.split("/")) {
       assert (!"".equals(part));
       TrieNode childNode = currentNode.findChild(part);
       if (childNode != null) {
@@ -253,17 +251,35 @@
       }
     }
 
-    if (mostSpecificPrefix == null) {
-      // Didn't match any specified prefix.
-      return false;
+    if (mostSpecificPrefix == null
+        || !mostSpecificPrefix.allows(resourceAbstractPathName)) {
+      /*
+       * Didn't match any specified prefix or the filter of the most specific
+       * prefix disallows the resource
+       */
+      return null;
     }
 
-    // Test the filter of the most specific prefix we found.
-    return mostSpecificPrefix.allows(resourceAbstractPathName);
+    return mostSpecificPrefix;
+  }
+
+  /**
+   * Returns true if the first pathPrefix is inserted into the PathPrefixSet
+   * after the second pathPrefix.
+   * 
+   */
+
+  public boolean secondPrefixOverridesFirst(PathPrefix prefix1,
+      PathPrefix prefix2) {
+    int rank1 = prefixes.get(prefix1);
+    assert rank1 > 0;
+    int rank2 = prefixes.get(prefix2);
+    assert rank2 > 0;
+    return rank2 > rank1;
   }
 
   public Collection<PathPrefix> values() {
-    return Collections.unmodifiableCollection(prefixes.values());
+    return Collections.unmodifiableCollection(prefixes.keySet());
   }
 
   private void assertValidAbstractDirectoryPathName(String name) {
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 502e634..13c569a 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
@@ -31,6 +31,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -267,19 +268,34 @@
      * precedence.
      */
     int changeCount = 0;
+    /**
+     * A map from AbstractResources (across all the ClassPathEntry) to their
+     * corresponding PathPrefixes. When old resources are used, an entry is
+     * added to this map from the old resource to the PathPrefix that matches
+     * the resource that the old resource supersedes.
+     */
+    Map<AbstractResource, PathPrefix> globalResourceToPrefixMap = new IdentityHashMap<AbstractResource, PathPrefix>();
     for (ClassPathEntry pathRoot : classPath) {
       TreeLogger branchForClassPathEntry = Messages.EXAMINING_PATH_ROOT.branch(
           refreshBranch, pathRoot.getLocation(), null);
 
       int prevChangeCount = changeCount;
-
-      Set<AbstractResource> newResources = pathRoot.findApplicableResources(
+      Map<AbstractResource, PathPrefix> resourceToPrefixMap = pathRoot.findApplicableResources(
           branchForClassPathEntry, pathPrefixSet);
-      for (AbstractResource newResource : newResources) {
+      globalResourceToPrefixMap.putAll(resourceToPrefixMap);
+      for (AbstractResource newResource : resourceToPrefixMap.keySet()) {
         String resourcePath = newResource.getPath();
 
-        // Make sure we don't already have a resource by this name.
-        if (newInternalMap.containsKey(resourcePath)) {
+        /*
+         * Check if we have a resource by this name in the newInternalMap and if
+         * the matching pathPrefix overrides the pathPrefix for the previous
+         * resource
+         */
+        AbstractResource existingNewResource = newInternalMap.get(resourcePath);
+        if (existingNewResource != null
+            && !pathPrefixSet.secondPrefixOverridesFirst(
+                globalResourceToPrefixMap.get(existingNewResource),
+                resourceToPrefixMap.get(newResource))) {
           Messages.IGNORING_SHADOWED_RESOURCE.log(branchForClassPathEntry,
               resourcePath, null);
           continue;
@@ -290,12 +306,18 @@
             newResource)) {
           newInternalMap.put(resourcePath, newResource);
           ++changeCount;
-        } else if (oldResource != null) {
-          // Nothing changed, so carry the identity of the old one forward.
+        } else {
+          assert oldResource != null;
+          /*
+           * Nothing changed, so carry the identity of the old one forward. Add
+           * the path-prefix to the global prefix map so that future resources
+           * can be compared.
+           */
           newInternalMap.put(resourcePath, oldResource);
+          globalResourceToPrefixMap.put(oldResource,
+              resourceToPrefixMap.get(newResource));
         }
       }
-
       if (changeCount == prevChangeCount) {
         Messages.NO_RESOURCES_CHANGED.log(branchForClassPathEntry, null);
       }
@@ -316,7 +338,8 @@
     }
 
     internalMap = newInternalMap;
-    Map<String, Resource> externalMap = rerootResourcePaths(newInternalMap);
+    Map<String, Resource> externalMap = rerootResourcePaths(newInternalMap,
+        globalResourceToPrefixMap);
 
     // Create a constant-time set for resources.
     Set<Resource> newResources = new HashSet<Resource>(externalMap.values());
@@ -333,50 +356,53 @@
   }
 
   private Map<String, Resource> rerootResourcePaths(
-      Map<String, AbstractResource> newInternalMap) {
-    Map<String, Resource> externalMap;
+      Map<String, AbstractResource> newInternalMap,
+      Map<AbstractResource, PathPrefix> resourceToPrefixMap) {
+
     // Create an external map with rebased path names.
-    externalMap = new HashMap<String, Resource>();
+    Map<String, Resource> externalMap = new HashMap<String, Resource>();
     for (AbstractResource resource : newInternalMap.values()) {
       String path = resource.getPath();
+      assert resourceToPrefixMap.get(resource) != null;
       if (externalMap.get(path) instanceof ResourceWrapper) {
-        // A rerooted resource blocks any other resource at this path.
-        continue;
-      }
-      int hitCount = 0;
-      for (PathPrefix pathPrefix : pathPrefixSet.values()) {
-        if (pathPrefix.allows(path)) {
-          assert (path.startsWith(pathPrefix.getPrefix()));
-          if (pathPrefix.shouldReroot()) {
-            String rerootedPath = pathPrefix.getRerootedPath(path);
-            if (externalMap.get(rerootedPath) instanceof ResourceWrapper) {
-              // A rerooted resource blocks any other resource at this path.
-              ++hitCount;
-              break;
-            }
-
-            // Try to reuse the same wrapper.
-            Resource exposed = exposedResourceMap.get(rerootedPath);
-            if (exposed instanceof ResourceWrapper) {
-              ResourceWrapper exposedWrapper = (ResourceWrapper) exposed;
-              if (exposedWrapper.resource == resource) {
-                externalMap.put(rerootedPath, exposedWrapper);
-                ++hitCount;
-                break;
-              }
-            }
-            // Just create a new wrapper.
-            AbstractResource wrapper = new ResourceWrapper(rerootedPath,
-                resource);
-            externalMap.put(rerootedPath, wrapper);
-            ++hitCount;
-          } else {
-            externalMap.put(path, resource);
-            ++hitCount;
-          }
+        // A rerooted resource can block any other resource at this path.
+        ResourceWrapper existingWrapper = (ResourceWrapper) externalMap.get(path);
+        if (!pathPrefixSet.secondPrefixOverridesFirst(
+            resourceToPrefixMap.get(existingWrapper.resource),
+            resourceToPrefixMap.get(resource))) {
+          continue;
         }
       }
-      assert (hitCount > 0);
+
+      PathPrefix pathPrefix = resourceToPrefixMap.get(resource);
+      assert pathPrefix != null;
+      if (pathPrefix.shouldReroot()) {
+        String rerootedPath = pathPrefix.getRerootedPath(path);
+        if (externalMap.get(rerootedPath) instanceof ResourceWrapper) {
+          ResourceWrapper existingWrapper = (ResourceWrapper) externalMap.get(rerootedPath);
+          if (!pathPrefixSet.secondPrefixOverridesFirst(
+              resourceToPrefixMap.get(existingWrapper.resource),
+              resourceToPrefixMap.get(resource))) {
+            // A rerooted resource blocks any other resource at this path.
+            continue;
+          }
+        }
+
+        // Try to reuse the same wrapper.
+        Resource exposed = exposedResourceMap.get(rerootedPath);
+        if (exposed instanceof ResourceWrapper) {
+          ResourceWrapper exposedWrapper = (ResourceWrapper) exposed;
+          if (exposedWrapper.resource == resource) {
+            externalMap.put(rerootedPath, exposedWrapper);
+            continue;
+          }
+        }
+        // Just create a new wrapper.
+        AbstractResource wrapper = new ResourceWrapper(rerootedPath, resource);
+        externalMap.put(rerootedPath, wrapper);
+      } else {
+        externalMap.put(path, resource);
+      }
     }
     return externalMap;
   }
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 6076260..f7a0420 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
@@ -21,6 +21,8 @@
 import java.io.File;
 import java.util.Enumeration;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -51,9 +53,10 @@
   }
 
   private Set<ZipFileResource> allZipFileResources;
-  private Set<AbstractResource> cachedAnswers;
+  private Map<AbstractResource, PathPrefix> cachedAnswers;
   private String cachedLocation;
   private PathPrefixSet lastPrefixSet;
+  private int lastPrefixSetSize;
   private final ZipFile zipFile;
 
   public ZipFileClassPathEntry(ZipFile zipFile) {
@@ -64,18 +67,19 @@
    * Indexes the zip file on-demand, and only once over the life of the process.
    */
   @Override
-  public Set<AbstractResource> findApplicableResources(TreeLogger logger,
-      PathPrefixSet pathPrefixSet) {
+  public Map<AbstractResource, PathPrefix> findApplicableResources(
+      TreeLogger logger, PathPrefixSet pathPrefixSet) {
     // Never re-index.
     if (allZipFileResources == null) {
       allZipFileResources = buildIndex(logger);
     }
 
     if (cachedAnswers == null || lastPrefixSet != pathPrefixSet
-        || lastPrefixSet.getModCount() != pathPrefixSet.getModCount()) {
+        || lastPrefixSetSize != pathPrefixSet.getSize()) {
       cachedAnswers = computeApplicableResources(logger, pathPrefixSet);
+      lastPrefixSet = pathPrefixSet;
+      lastPrefixSetSize = pathPrefixSet.getSize();
     }
-
     return cachedAnswers;
   }
 
@@ -113,17 +117,18 @@
     return results;
   }
 
-  private Set<AbstractResource> computeApplicableResources(TreeLogger logger,
-      PathPrefixSet pathPrefixSet) {
+  private Map<AbstractResource, PathPrefix> computeApplicableResources(
+      TreeLogger logger, PathPrefixSet pathPrefixSet) {
     logger = Messages.FINDING_INCLUDED_RESOURCES.branch(logger,
         zipFile.getName(), null);
 
-    Set<AbstractResource> results = new HashSet<AbstractResource>();
+    Map<AbstractResource, PathPrefix> results = new IdentityHashMap<AbstractResource, PathPrefix>();
     for (ZipFileResource r : allZipFileResources) {
       String path = r.getPath();
-      if (pathPrefixSet.includesResource(path)) {
+      PathPrefix prefix = null;
+      if ((prefix = pathPrefixSet.includesResource(path)) != null) {
         Messages.INCLUDING_RESOURCE.log(logger, path, null);
-        results.add(r);
+        results.put(r, prefix);
       } else {
         Messages.EXCLUDING_RESOURCE.log(logger, path, null);
       }
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/AbstractResourceOrientedTestBase.java b/dev/core/test/com/google/gwt/dev/resource/impl/AbstractResourceOrientedTestBase.java
index 489e571..29c9feb 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/AbstractResourceOrientedTestBase.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/AbstractResourceOrientedTestBase.java
@@ -25,7 +25,8 @@
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.jar.JarFile;
 
@@ -67,14 +68,14 @@
     }
 
     @Override
-    public Set<AbstractResource> findApplicableResources(TreeLogger logger,
-        PathPrefixSet pathPrefixSet) {
-      Set<AbstractResource> results = new HashSet<AbstractResource>();
-      Set<AbstractResource> rs = cpe.findApplicableResources(logger,
-          pathPrefixSet);
-      for (AbstractResource r : rs) {
+    public Map<AbstractResource, PathPrefix> findApplicableResources(
+        TreeLogger logger, PathPrefixSet pathPrefixSet) {
+      Map<AbstractResource, PathPrefix> results = new IdentityHashMap<AbstractResource, PathPrefix>();
+      Map<AbstractResource, PathPrefix> rs = cpe.findApplicableResources(
+          logger, pathPrefixSet);
+      for (AbstractResource r : rs.keySet()) {
         if (r.getPath().indexOf(".svn/") < 0) {
-          results.add(r);
+          results.put(r, rs.get(r));
         }
       }
       return results;
@@ -114,6 +115,7 @@
       addResource("com/google/gwt/i18n/rebind/LocalizableGenerator.java");
       addResource("org/example/bar/client/BarClient2.txt");
       addResource("org/example/bar/client/BarClient3.txt");
+      addResource("org/example/foo/client/BarClient1.txt");
     }
   }
 
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java
index 3454176..5bfddad 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java
@@ -81,7 +81,7 @@
 
     {
       // Examine cpe1.
-      Set<AbstractResource> r = cpe1.findApplicableResources(logger, pps);
+      Set<AbstractResource> r = cpe1.findApplicableResources(logger, pps).keySet();
 
       assertEquals(3, r.size());
       assertPathIncluded(r, "com/google/gwt/user/client/Command.java");
@@ -91,7 +91,7 @@
 
     {
       // Examine cpe2.
-      Set<AbstractResource> r = cpe2.findApplicableResources(logger, pps);
+      Set<AbstractResource> r = cpe2.findApplicableResources(logger, pps).keySet();
 
       assertEquals(1, r.size());
       assertPathIncluded(r, "com/google/gwt/i18n/client/Messages.java");
@@ -106,7 +106,7 @@
     PathPrefixSet pps = new PathPrefixSet();
     pps.add(new PathPrefix("", null));
 
-    Set<AbstractResource> r = cpe1.findApplicableResources(logger, pps);
+    Set<AbstractResource> r = cpe1.findApplicableResources(logger, pps).keySet();
 
     assertEquals(9, r.size());
     assertPathIncluded(r, "com/google/gwt/user/User.gwt.xml");
@@ -127,15 +127,16 @@
 
     PathPrefixSet pps = new PathPrefixSet();
     pps.add(new PathPrefix("", null));
-    Set<AbstractResource> r = cpe2.findApplicableResources(logger, pps);
+    Set<AbstractResource> r = cpe2.findApplicableResources(logger, pps).keySet();
 
-    assertEquals(5, r.size());
+    assertEquals(6, r.size());
     assertPathIncluded(r, "com/google/gwt/i18n/I18N.gwt.xml");
     assertPathIncluded(r, "com/google/gwt/i18n/client/Messages.java");
     assertPathIncluded(r,
         "com/google/gwt/i18n/rebind/LocalizableGenerator.java");
     assertPathIncluded(r, "org/example/bar/client/BarClient2.txt");
     assertPathIncluded(r, "org/example/bar/client/BarClient3.txt");
+    assertPathIncluded(r, "org/example/foo/client/BarClient1.txt");
   }
 
   private void testPathPrefixSetChanges(ClassPathEntry cpe1, ClassPathEntry cpe2) {
@@ -148,7 +149,7 @@
       pps.add(new PathPrefix("com/google/gwt/i18n/", null));
 
       // Examine cpe1 in the absence of the filter.
-      Set<AbstractResource> r1 = cpe1.findApplicableResources(logger, pps);
+      Set<AbstractResource> r1 = cpe1.findApplicableResources(logger, pps).keySet();
 
       assertEquals(4, r1.size());
       assertPathIncluded(r1, "com/google/gwt/user/User.gwt.xml");
@@ -157,7 +158,7 @@
       assertPathIncluded(r1, "com/google/gwt/user/client/ui/Widget.java");
 
       // Examine cpe2 in the absence of the filter.
-      Set<AbstractResource> r2 = cpe2.findApplicableResources(logger, pps);
+      Set<AbstractResource> r2 = cpe2.findApplicableResources(logger, pps).keySet();
 
       assertEquals(3, r2.size());
       assertPathIncluded(r2, "com/google/gwt/i18n/I18N.gwt.xml");
@@ -179,7 +180,7 @@
       pps.add(new PathPrefix("com/google/gwt/i18n/", excludeXmlFiles));
 
       // Examine cpe1 in the presence of the filter.
-      Set<AbstractResource> r1 = cpe1.findApplicableResources(logger, pps);
+      Set<AbstractResource> r1 = cpe1.findApplicableResources(logger, pps).keySet();
 
       assertEquals(3, r1.size());
       assertPathNotIncluded(r1, "com/google/gwt/user/User.gwt.xml");
@@ -188,7 +189,7 @@
       assertPathIncluded(r1, "com/google/gwt/user/client/ui/Widget.java");
 
       // Examine cpe2 in the presence of the filter.
-      Set<AbstractResource> r2 = cpe2.findApplicableResources(logger, pps);
+      Set<AbstractResource> r2 = cpe2.findApplicableResources(logger, pps).keySet();
 
       assertEquals(2, r2.size());
       assertPathNotIncluded(r1, "com/google/gwt/user/User.gwt.xml");
@@ -211,12 +212,12 @@
       }));
 
       // Examine cpe1 in the presence of the filter.
-      Set<AbstractResource> r1 = cpe1.findApplicableResources(logger, pps);
+      Set<AbstractResource> r1 = cpe1.findApplicableResources(logger, pps).keySet();
 
       assertEquals(0, r1.size());
 
       // Examine cpe2 in the presence of the filter.
-      Set<AbstractResource> r2 = cpe2.findApplicableResources(logger, pps);
+      Set<AbstractResource> r2 = cpe2.findApplicableResources(logger, pps).keySet();
 
       assertEquals(0, r2.size());
     }
@@ -238,7 +239,7 @@
 
     {
       // Examine cpe1.
-      Set<AbstractResource> r = cpe1.findApplicableResources(logger, pps);
+      Set<AbstractResource> r = cpe1.findApplicableResources(logger, pps).keySet();
 
       assertEquals(3, r.size());
       // User.gwt.xml would be included but for the filter.
@@ -249,7 +250,7 @@
 
     {
       // Examine cpe2.
-      Set<AbstractResource> r = cpe2.findApplicableResources(logger, pps);
+      Set<AbstractResource> r = cpe2.findApplicableResources(logger, pps).keySet();
 
       assertEquals(2, r.size());
       // I18N.gwt.xml would be included but for the filter.
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/MockClassPathEntry.java b/dev/core/test/com/google/gwt/dev/resource/impl/MockClassPathEntry.java
index 913b071..254c0f9 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/MockClassPathEntry.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/MockClassPathEntry.java
@@ -21,9 +21,8 @@
 import junit.framework.Assert;
 
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.Map;
-import java.util.Set;
 
 public class MockClassPathEntry extends ClassPathEntry {
 
@@ -46,14 +45,15 @@
   }
 
   @Override
-  public Set<AbstractResource> findApplicableResources(TreeLogger logger,
-      PathPrefixSet pathPrefixes) {
+  public Map<AbstractResource, PathPrefix> findApplicableResources(
+      TreeLogger logger, PathPrefixSet pathPrefixes) {
     // Only include resources that have the prefix and pass its filter.
-    HashSet<AbstractResource> results = new HashSet<AbstractResource>();
+    Map<AbstractResource, PathPrefix> results = new IdentityHashMap<AbstractResource, PathPrefix>();
     for (Map.Entry<String, MockAbstractResource> entry : resourceMap.entrySet()) {
       String path = entry.getKey();
-      if (pathPrefixes.includesResource(path)) {
-        results.add(entry.getValue());
+      PathPrefix prefix = null;
+      if ((prefix = pathPrefixes.includesResource(path)) != null) {
+        results.put(entry.getValue(), prefix);
       }
     }
 
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/PathPrefixSetTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/PathPrefixSetTest.java
index e9d581c..8c73c99 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/PathPrefixSetTest.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/PathPrefixSetTest.java
@@ -24,154 +24,181 @@
 
   public void testEmptyPrefixSet() {
     PathPrefixSet pps = new PathPrefixSet();
-    assertFalse(pps.includesResource("com/google/gwt/user/client/Command.java"));
+    assertNull(pps.includesResource("com/google/gwt/user/client/Command.java"));
   }
 
-  public void testNonOverlappingPrefixes() {
-    {
-      /*
-       * Test with null filters to ensure nothing gets filtered out.
-       */
-      PathPrefixSet pps = new PathPrefixSet();
-      pps.add(new PathPrefix("com/google/gwt/user/client/", null));
-      pps.add(new PathPrefix("com/google/gwt/i18n/client/", null));
-      pps.add(new PathPrefix("com/google/gwt/dom/client/", null));
+  public void testNonOverlappingPrefixesEmptyFilter() {
+    /*
+     * Test with null filters to ensure nothing gets filtered out.
+     */
+    PathPrefixSet pps = new PathPrefixSet();
+    PathPrefix pp1 = new PathPrefix("com/google/gwt/user/client/", null);
+    PathPrefix pp2 = new PathPrefix("com/google/gwt/i18n/client/", null);
+    PathPrefix pp3 = new PathPrefix("com/google/gwt/dom/client/", null);
+    pps.add(pp1);
+    pps.add(pp2);
+    pps.add(pp3);
 
-      assertTrue(pps.includesDirectory("com/"));
-      assertTrue(pps.includesDirectory("com/google/"));
-      assertTrue(pps.includesDirectory("com/google/gwt/"));
-      assertTrue(pps.includesDirectory("com/google/gwt/user/"));
-      assertTrue(pps.includesDirectory("com/google/gwt/user/client/"));
-      assertTrue(pps.includesDirectory("com/google/gwt/user/client/ui/"));
+    assertTrue(pps.includesDirectory("com/"));
+    assertTrue(pps.includesDirectory("com/google/"));
+    assertTrue(pps.includesDirectory("com/google/gwt/"));
+    assertTrue(pps.includesDirectory("com/google/gwt/user/"));
+    assertTrue(pps.includesDirectory("com/google/gwt/user/client/"));
+    assertTrue(pps.includesDirectory("com/google/gwt/user/client/ui/"));
 
-      assertFalse(pps.includesDirectory("org/"));
-      assertFalse(pps.includesDirectory("org/example/"));
-      assertFalse(pps.includesDirectory("com/google/gwt/user/server/"));
-      assertFalse(pps.includesDirectory("com/google/gwt/xml/client/"));
+    assertFalse(pps.includesDirectory("org/"));
+    assertFalse(pps.includesDirectory("org/example/"));
+    assertFalse(pps.includesDirectory("com/google/gwt/user/server/"));
+    assertFalse(pps.includesDirectory("com/google/gwt/xml/client/"));
 
-      assertTrue(pps.includesResource("com/google/gwt/user/client/Command.java"));
-      assertTrue(pps.includesResource("com/google/gwt/user/client/Timer.java"));
-      assertTrue(pps.includesResource("com/google/gwt/i18n/client/Messages.java"));
-      assertTrue(pps.includesResource("com/google/gwt/dom/client/DivElement.java"));
+    assertEquals(pp1,
+        pps.includesResource("com/google/gwt/user/client/Command.java"));
+    assertEquals(pp1,
+        pps.includesResource("com/google/gwt/user/client/Timer.java"));
+    assertEquals(pp2,
+        pps.includesResource("com/google/gwt/i18n/client/Messages.java"));
+    assertEquals(pp3,
+        pps.includesResource("com/google/gwt/dom/client/DivElement.java"));
 
-      assertFalse(pps.includesResource("com/google/gwt/user/rebind/rpc/ServiceInterfaceProxyGenerator.java"));
-      assertFalse(pps.includesResource("com/google/gwt/sample/hello/client/Hello.java"));
-      assertFalse(pps.includesResource("com/google/gwt/user/public/clear.cache.gif"));
-    }
-
-    {
-      /*
-       * Test with a real filter to ensure it does have an effect.
-       */
-      PathPrefixSet pps = new PathPrefixSet();
-      ResourceFilter allowsGifs = new ResourceFilter() {
-        public boolean allows(String path) {
-          return path.toLowerCase().endsWith(".gif");
-        }
-      };
-
-      pps.add(new PathPrefix("com/google/gwt/user/public/", allowsGifs));
-      pps.add(new PathPrefix("com/google/gwt/sample/mail/public/", allowsGifs));
-
-      // Correct prefix, and filter should allow .
-      assertTrue(pps.includesResource("com/google/gwt/user/public/clear.cache.gif"));
-      assertTrue(pps.includesResource("com/google/gwt/sample/mail/public/inboxIcon.gif"));
-
-      // Correct prefix, but filter should exclude.
-      assertFalse(pps.includesResource("com/google/gwt/user/public/README.txt"));
-      assertFalse(pps.includesResource("com/google/gwt/sample/mail/public/README.txt"));
-
-      // Wrong prefix, and filter would have excluded.
-      assertFalse(pps.includesResource("com/google/gwt/user/client/Command.java"));
-      assertFalse(pps.includesResource("com/google/gwt/user/rebind/rpc/ServiceInterfaceProxyGenerator.java"));
-
-      // Wrong prefix, but filter would have allowed it.
-      assertFalse(pps.includesResource("com/google/gwt/i18n/public/flags.gif"));
-    }
+    assertNull(pps.includesResource("com/google/gwt/user/rebind/rpc/ServiceInterfaceProxyGenerator.java"));
+    assertNull(pps.includesResource("com/google/gwt/sample/hello/client/Hello.java"));
+    assertNull(pps.includesResource("com/google/gwt/user/public/clear.cache.gif"));
   }
 
-  public void testOverlappingPrefixes() {
-    {
-      /*
-       * Without a filter.
-       */
-      PathPrefixSet pps = new PathPrefixSet();
-      pps.add(new PathPrefix("", null));
-      pps.add(new PathPrefix("a/", null));
-      pps.add(new PathPrefix("a/b/", null));
-      pps.add(new PathPrefix("a/b/c/", null));
+  public void testNonOverlappingPrefixesNonEmptyFilter() {
+    /*
+     * Test with a real filter to ensure it does have an effect.
+     */
+    PathPrefixSet pps = new PathPrefixSet();
+    ResourceFilter allowsGifs = new ResourceFilter() {
+      public boolean allows(String path) {
+        return path.toLowerCase().endsWith(".gif");
+      }
+    };
 
-      assertTrue(pps.includesResource("W.java"));
-      assertTrue(pps.includesResource("a/X.java"));
-      assertTrue(pps.includesResource("a/b/Y.java"));
-      assertTrue(pps.includesResource("a/b/c/Z.java"));
-      assertTrue(pps.includesResource("a/b/c/d/V.java"));
-    }
+    PathPrefix pp1 = new PathPrefix("com/google/gwt/user/public/", allowsGifs);
+    PathPrefix pp2 = new PathPrefix("com/google/gwt/sample/mail/public/",
+        allowsGifs);
+    pps.add(pp1);
+    pps.add(pp2);
 
-    {
-      /*
-       * Ensure the right filter applies.
-       */
-      PathPrefixSet pps = new PathPrefixSet();
-      pps.add(new PathPrefix("", null));
-      pps.add(new PathPrefix("a/", null));
-      pps.add(new PathPrefix("a/b/", new ResourceFilter() {
-        public boolean allows(String path) {
-          // Disallow anything ending with "FILTERMEOUT".
-          return !path.endsWith("FILTERMEOUT");
-        }
-      }));
-      pps.add(new PathPrefix("a/b/c/", null));
+    // Correct prefix, and filter should allow .
+    assertEquals(pp1,
+        pps.includesResource("com/google/gwt/user/public/clear.cache.gif"));
+    assertEquals(pp2,
+        pps.includesResource("com/google/gwt/sample/mail/public/inboxIcon.gif"));
 
-      assertTrue(pps.includesResource("W.java"));
-      assertTrue(pps.includesResource("a/X.java"));
-      assertTrue(pps.includesResource("a/b/Y.java"));
-      // This should be gone, since it is found in b.
-      assertFalse(pps.includesResource("a/b/FILTERMEOUT"));
-      /*
-       * This should not be gone, because it is using c's (null) filter instead
-       * of b's. The logic here is that the prefix including c is more specific
-       * and seemed to want c's resources to be included.
-       */
-      assertTrue(pps.includesResource("a/b/c/DONT_FILTERMEOUT"));
-      assertTrue(pps.includesResource("a/b/c/Z.java"));
-      assertTrue(pps.includesResource("a/b/c/d/V.java"));
-    }
+    // Correct prefix, but filter should exclude.
+    assertNull(pps.includesResource("com/google/gwt/user/public/README.txt"));
+    assertNull(pps.includesResource("com/google/gwt/sample/mail/public/README.txt"));
+
+    // Wrong prefix, and filter would have excluded.
+    assertNull(pps.includesResource("com/google/gwt/user/client/Command.java"));
+    assertNull(pps.includesResource("com/google/gwt/user/rebind/rpc/ServiceInterfaceProxyGenerator.java"));
+
+    // Wrong prefix, but filter would have allowed it.
+    assertNull(pps.includesResource("com/google/gwt/i18n/public/flags.gif"));
+  }
+
+  public void testOverlappingPrefixesEmptyFilter() {
+    /*
+     * Without a filter.
+     */
+    PathPrefixSet pps = new PathPrefixSet();
+    PathPrefix pp1 = new PathPrefix("a/b/", null);
+    PathPrefix pp2 = new PathPrefix("a/", null);
+    PathPrefix pp3 = new PathPrefix("", null);
+    PathPrefix pp4 = new PathPrefix("a/b/c/", null);
+    PathPrefix pp5 = new PathPrefix("a/", null);
+    pps.add(pp1);
+    pps.add(pp2);
+    pps.add(pp3);
+    pps.add(pp4);
+    // pp5 now overrides pp2
+    pps.add(pp5);
+
+    assertEquals(pp3, pps.includesResource("W.java"));
+    assertEquals(pp5, pps.includesResource("a/X.java"));
+    assertEquals(pp1, pps.includesResource("a/b/Y.java"));
+    assertEquals(pp4, pps.includesResource("a/b/c/Z.java"));
+    assertEquals(pp4, pps.includesResource("a/b/c/d/V.java"));
+  }
+
+  public void testOverlappingPrefixesNonEmptyFilter() {
+    /*
+     * Ensure the right filter applies.
+     */
+    PathPrefixSet pps = new PathPrefixSet();
+    PathPrefix pp1 = new PathPrefix("", null);
+    PathPrefix pp2 = new PathPrefix("a/", null);
+    PathPrefix pp3 = new PathPrefix("a/b/", new ResourceFilter() {
+      public boolean allows(String path) {
+        // Disallow anything ending with "FILTERMEOUT".
+        return !path.endsWith("FILTERMEOUT");
+      }
+    });
+    PathPrefix pp4 = new PathPrefix("a/b/c/", null);
+    PathPrefix pp5 = new PathPrefix("a/", new ResourceFilter() {
+      public boolean allows(String path) {
+        return !path.endsWith("X.java");
+      }
+    });
+    pps.add(pp1);
+    pps.add(pp2);
+    pps.add(pp3);
+    pps.add(pp4);
+    pps.add(pp5);
+
+    assertEquals(pp1, pps.includesResource("W.java"));
+
+    // see TODO in the implementation note for PathPrefixSet.java
+    // assertEquals(pp2, pps.includesResource("a/X.java"));
+    assertEquals(pp5, pps.includesResource("a/Y.java"));
+    assertEquals(pp3, pps.includesResource("a/b/Y.java"));
+    // This should be gone, since it is found in b.
+    assertNull(pps.includesResource("a/b/FILTERMEOUT"));
+    /*
+     * This should not be gone, because it is using c's (null) filter instead of
+     * b's. The logic here is that the prefix including c is more specific and
+     * seemed to want c's resources to be included.
+     */
+    assertEquals(pp4, pps.includesResource("a/b/c/DONT_FILTERMEOUT"));
+    assertEquals(pp4, pps.includesResource("a/b/c/Z.java"));
+    assertEquals(pp4, pps.includesResource("a/b/c/d/V.java"));
   }
 
   /**
-   * In essense, this tests support for the default package in Java.
+   * In essence, this tests support for the default package in Java.
    */
-  public void testZeroLengthPrefix() {
-    {
-      /*
-       * Without a filter.
-       */
-      PathPrefixSet pps = new PathPrefixSet();
-      pps.add(new PathPrefix("", null));
+  public void testZeroLengthPrefixEmptyFilter() {
+    /*
+     * Without a filter.
+     */
+    PathPrefixSet pps = new PathPrefixSet();
+    PathPrefix pp1 = new PathPrefix("", null);
+    pps.add(pp1);
 
-      assertTrue(pps.includesResource("W.java"));
-      assertTrue(pps.includesResource("a/X.java"));
-      assertTrue(pps.includesResource("a/b/Y.java"));
-      assertTrue(pps.includesResource("a/b/c/Z.java"));
-    }
+    assertEquals(pp1, pps.includesResource("W.java"));
+    assertEquals(pp1, pps.includesResource("a/X.java"));
+    assertEquals(pp1, pps.includesResource("a/b/Y.java"));
+    assertEquals(pp1, pps.includesResource("a/b/c/Z.java"));
+  }
 
-    {
-      /*
-       * With a filter.
-       */
-      PathPrefixSet pps = new PathPrefixSet();
-      pps.add(new PathPrefix("", new ResourceFilter() {
-        public boolean allows(String path) {
-          return path.endsWith("Y.java");
-        }
-      }));
+  public void testZeroLengthPrefixNonEmptyFilter() {
+    /*
+     * With a filter.
+     */
+    PathPrefixSet pps = new PathPrefixSet();
+    PathPrefix pp1 = new PathPrefix("", new ResourceFilter() {
+      public boolean allows(String path) {
+        return path.endsWith("Y.java");
+      }
+    });
+    pps.add(pp1);
 
-      assertFalse(pps.includesResource("W.java"));
-      assertFalse(pps.includesResource("a/X.java"));
-      assertTrue(pps.includesResource("a/b/Y.java"));
-      assertFalse(pps.includesResource("a/b/c/Z.java"));
-
-    }
+    assertNull(pps.includesResource("W.java"));
+    assertNull(pps.includesResource("a/X.java"));
+    assertEquals(pp1, pps.includesResource("a/b/Y.java"));
+    assertNull(pps.includesResource("a/b/c/Z.java"));
   }
 }
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/ResourceOracleImplTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/ResourceOracleImplTest.java
index 46e166d..a4b1912 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/ResourceOracleImplTest.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/ResourceOracleImplTest.java
@@ -164,6 +164,62 @@
   }
 
   /**
+   * Test that ResourceOracleImpl prefers the order of path prefixes over
+   * ClassPathEntries.
+   * <p>
+   * cpe1 contains org/example/bar/client/BarClient1.txt and cpe2 contains
+   * org/example/foo/client/BarClient1.txt
+   * 
+   * @throws URISyntaxException
+   * @throws IOException
+   */
+  public void testPathPrefixOrderPreferredOverClasspath() throws IOException,
+      URISyntaxException {
+    TreeLogger logger = createTestTreeLogger();
+    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
+    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();
+
+    ClassPathEntry[] cp12 = new ClassPathEntry[] {cpe1jar, cpe2jar};
+    ClassPathEntry[] cp21 = new ClassPathEntry[] {cpe2jar, cpe1jar};
+
+    String barKeyNormal = "org/example/bar/client/BarClient1.txt";
+    String keyReroot = "/BarClient1.txt";
+    String fooKeyNormal = "org/example/foo/client/BarClient1.txt";
+
+    PathPrefix pathPrefix1Reroot = new PathPrefix("org/example/bar/client",
+        null, true);
+    PathPrefix pathPrefix2Reroot = new PathPrefix("org/example/foo/client",
+        null, true);
+
+    PathPrefixSet pps12Reroot = new PathPrefixSet();
+    pps12Reroot.add(pathPrefix1Reroot);
+    pps12Reroot.add(pathPrefix2Reroot);
+
+    PathPrefixSet pps21Reroot = new PathPrefixSet();
+    pps21Reroot.add(pathPrefix2Reroot);
+    pps21Reroot.add(pathPrefix1Reroot);
+
+    /*
+     * the keyReroot resource in cpe2 wins because pathPrefix2Reroot comes
+     * later.
+     */
+    testPathPrefixOrderPreferredOverClasspath(logger, keyReroot, cp12,
+        pps12Reroot, 1);
+    // order of specifying classpath is reversed, it still matches cpe2
+    testPathPrefixOrderPreferredOverClasspath(logger, keyReroot, cp21,
+        pps12Reroot, 0);
+    /*
+     * the keyReroot resource in cpe2 wins because pathPrefix2Reroot comes
+     * later.
+     */
+    testPathPrefixOrderPreferredOverClasspath(logger, keyReroot, cp12,
+        pps21Reroot, 0);
+    // order of specifying classpath is reversed, it still matches cpe1
+    testPathPrefixOrderPreferredOverClasspath(logger, keyReroot, cp21,
+        pps21Reroot, 1);
+  }
+
+  /**
    * Tests the actual reading of resources.
    * 
    * @throws URISyntaxException
@@ -273,11 +329,15 @@
     oracle.setPathPrefixes(pps);
     return oracle;
   }
-
+  
   private ResourceOracleSnapshot refreshAndSnapshot(TreeLogger logger,
       ResourceOracleImpl oracle) {
     oracle.refresh(logger);
-    return new ResourceOracleSnapshot(oracle);
+    ResourceOracleSnapshot snapshot1 = new ResourceOracleSnapshot(oracle);
+    oracle.refresh(logger);
+    ResourceOracleSnapshot snapshot2 = new ResourceOracleSnapshot(oracle);
+    snapshot1.assertSameCollections(snapshot2);
+    return snapshot1;
   }
 
   private void testClassPathOrderIsHonored(TreeLogger logger,
@@ -290,13 +350,22 @@
     s.assertPathIncluded(resourceKey, classPath[0]);
   }
 
+  private void testPathPrefixOrderPreferredOverClasspath(TreeLogger logger,
+      String resourceKey, ClassPathEntry[] classPath, PathPrefixSet pps,
+      int index) {
+    ResourceOracleImpl oracle = new ResourceOracleImpl(Arrays.asList(classPath));
+    oracle.setPathPrefixes(pps);
+    ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
+    s.assertPathIncluded(resourceKey, classPath[index]);
+  }
+
   private void testReadingResource(ClassPathEntry cpe1, ClassPathEntry cpe2)
       throws IOException {
     TreeLogger logger = createTestTreeLogger();
 
     ResourceOracleImpl oracle = createResourceOracle(cpe1, cpe2);
     ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
-    s.assertCollectionsConsistent(9);
+    s.assertCollectionsConsistent(10);
     s.assertPathIncluded("com/google/gwt/user/client/Command.java", cpe1);
     s.assertPathIncluded("com/google/gwt/i18n/client/Messages.java", cpe2);
 
@@ -354,7 +423,7 @@
        * assumptions about the contents of each classpath entry.
        */
       ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
-      s.assertCollectionsConsistent(9);
+      s.assertCollectionsConsistent(10);
       s.assertPathIncluded("com/google/gwt/user/client/Command.java");
       s.assertPathIncluded("com/google/gwt/user/client/Timer.java");
       s.assertPathIncluded("com/google/gwt/user/client/ui/Widget.java");
@@ -377,7 +446,7 @@
 
       ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
       ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
-      after.assertCollectionsConsistent(9);
+      after.assertCollectionsConsistent(10);
       after.assertSameCollections(before);
     }
 
@@ -390,7 +459,7 @@
 
       ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
       ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
-      after.assertCollectionsConsistent(10);
+      after.assertCollectionsConsistent(11);
       after.assertNotSameCollections(before);
       after.assertPathIncluded("com/google/gwt/i18n/client/Constants.java");
     }
@@ -417,7 +486,7 @@
        * assumptions about the contents of each classpath entry.
        */
       ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
-      s.assertCollectionsConsistent(10);
+      s.assertCollectionsConsistent(11);
       s.assertPathIncluded("com/google/gwt/user/client/Command.java");
       s.assertPathIncluded("com/google/gwt/user/client/Timer.java");
       s.assertPathIncluded("com/google/gwt/user/client/ui/Widget.java");
@@ -440,7 +509,7 @@
 
       ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
       ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
-      after.assertCollectionsConsistent(10);
+      after.assertCollectionsConsistent(11);
       after.assertSameCollections(before);
     }
 
@@ -455,7 +524,7 @@
 
       ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
       ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
-      after.assertCollectionsConsistent(10);
+      after.assertCollectionsConsistent(11);
       after.assertNotSameCollections(before);
       after.assertPathIncluded("com/google/gwt/user/client/Window.java");
       after.assertPathNotIncluded("com/google/gwt/i18n/client/Constants.java");
@@ -497,7 +566,7 @@
        * isn't observed.
        */
       ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
-      before.assertCollectionsConsistent(9);
+      before.assertCollectionsConsistent(10);
 
       cpe3.updateResource("com/google/gwt/user/client/Timer.java");
 
@@ -511,7 +580,7 @@
        * is observed.
        */
       ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
-      before.assertCollectionsConsistent(9);
+      before.assertCollectionsConsistent(10);
 
       cpe0.updateResource("com/google/gwt/user/client/Command.java");
 
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1.jar b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1.jar
index 5045c12..c5127c9 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1.jar
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe1.jar
Binary files differ
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2.jar b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2.jar
index 08761bb..2e6663b 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2.jar
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2.jar
Binary files differ
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2/org/example/foo/client/BarClient1.txt b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2/org/example/foo/client/BarClient1.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/testdata/cpe2/org/example/foo/client/BarClient1.txt
@@ -0,0 +1 @@
+