Ignore duplicate classpath entries, and share classpaths generated from the same URLClassLoader.

This will reduce memory and processing requirements for build systems where many classpath entries
get duplicated.

Patch by: jat, amitmanjhi (pair programmed)
Review by: rjrjr



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@4592 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 f963fb0..50a1985 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
@@ -42,9 +42,9 @@
 /**
  * The normal implementation of {@link ResourceOracle}.
  * 
- * TODO: this incorporates a quick-and-dirty fix for issue 3078 -- a proper
- * fix that considers module inheritance order before classpath order should
- * be implemented.
+ * TODO: this incorporates a quick-and-dirty fix for issue 3078 -- a proper fix
+ * that considers module inheritance order before classpath order should be
+ * implemented.
  */
 public class ResourceOracleImpl implements ResourceOracle {
 
@@ -130,6 +130,8 @@
     }
   }
 
+  private static final Map<ClassLoader, List<ClassPathEntry>> classPathCache = new HashMap<ClassLoader, List<ClassPathEntry>>();
+
   public static ClassPathEntry createEntryForUrl(TreeLogger logger, URL url)
       throws URISyntaxException, IOException {
     if (url.getProtocol().equals("file")) {
@@ -164,11 +166,16 @@
 
   private static void addAllClassPathEntries(TreeLogger logger,
       ClassLoader classLoader, List<ClassPathEntry> classPath) {
+    Set<URL> seenEntries = new HashSet<URL>();
     for (; classLoader != null; classLoader = classLoader.getParent()) {
       if (classLoader instanceof URLClassLoader) {
         URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
         URL[] urls = urlClassLoader.getURLs();
         for (URL url : urls) {
+          if (seenEntries.contains(url)) {
+            continue;
+          }
+          seenEntries.add(url);
           Throwable caught;
           try {
             ClassPathEntry entry = createEntryForUrl(logger, url);
@@ -177,7 +184,8 @@
             }
             continue;
           } catch (AccessControlException e) {
-            logger.log(TreeLogger.DEBUG, "Skipping URL due to access restrictions: " + url);
+            logger.log(TreeLogger.DEBUG,
+                "Skipping URL due to access restrictions: " + url);
             continue;
           } catch (URISyntaxException e) {
             caught = e;
@@ -191,10 +199,14 @@
     }
   }
 
-  private static List<ClassPathEntry> getAllClassPathEntries(TreeLogger logger,
-      ClassLoader classLoader) {
-    ArrayList<ClassPathEntry> classPath = new ArrayList<ClassPathEntry>();
-    addAllClassPathEntries(logger, classLoader, classPath);
+  private static synchronized List<ClassPathEntry> getAllClassPathEntries(
+      TreeLogger logger, ClassLoader classLoader) {
+    List<ClassPathEntry> classPath = classPathCache.get(classLoader);
+    if (classPath == null) {
+      classPath = new ArrayList<ClassPathEntry>();
+      addAllClassPathEntries(logger, classLoader, classPath);
+      classPathCache.put(classLoader, classPath);
+    }
     return classPath;
   }
 
@@ -340,6 +352,11 @@
     this.pathPrefixSet = pathPrefixSet;
   }
 
+  // @VisibleForTesting
+  List<ClassPathEntry> getClassPath() {
+    return classPath;
+  }
+
   private Map<String, Resource> rerootResourcePaths(
       Map<String, AbstractResource> newInternalMap) {
     Map<String, Resource> externalMap;
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..c72d651 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
@@ -152,8 +152,7 @@
   }
 
   protected File findJarFile(String name) throws URISyntaxException {
-    ClassLoader classLoader = getClass().getClassLoader();
-    URL url = classLoader.getResource(name);
+    URL url = findJarUrl(name);
     assertNotNull(
         "Expecting on the classpath: "
             + name
@@ -164,6 +163,15 @@
     return file;
   }
 
+  /**
+   * @param name
+   * @return
+   */
+  protected URL findJarUrl(String name) {
+    ClassLoader classLoader = getClass().getClassLoader();
+    return classLoader.getResource(name);
+  }
+
   protected Resource findResourceWithPath(Set<AbstractResource> resources,
       String path) {
     for (Resource r : resources) {
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..8449d7d 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
@@ -23,7 +23,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.net.MalformedURLException;
 import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -183,6 +186,32 @@
     testReadingResource(cpe1dir, cpe2dir);
   }
 
+  /**
+   * Verify that duplicate entries are removed from the classpath, and that
+   * multiple ResourceOracleImpls created from the same classloader return the
+   * same list of ClassPathEntries.
+   * 
+   * @throws MalformedURLException
+   */
+  public void testRemoveDuplicates() throws MalformedURLException {
+    TreeLogger logger = createTestTreeLogger();
+    URL cpe1 = findJarUrl("com/google/gwt/dev/resource/impl/testdata/cpe1.jar");
+    URL cpe2 = findJarUrl("com/google/gwt/dev/resource/impl/testdata/cpe2.jar");
+    URLClassLoader classLoader = new URLClassLoader(new URL[] {
+        cpe1, cpe2, cpe2, cpe1, cpe2,}, null);
+    ResourceOracleImpl oracle = new ResourceOracleImpl(logger, classLoader);
+    List<ClassPathEntry> classPath = oracle.getClassPath();
+    assertEquals(2, classPath.size());
+    assertJarEntry(classPath.get(0), "cpe1.jar");
+    assertJarEntry(classPath.get(1), "cpe2.jar");
+    oracle = new ResourceOracleImpl(logger, classLoader);
+    List<ClassPathEntry> classPath2 = oracle.getClassPath();
+    assertEquals(2, classPath2.size());
+    for (int i = 0; i < 2; ++i) {
+      assertSame(classPath.get(i), classPath2.get(i));
+    }
+  }
+
   public void testResourceAddition() throws IOException, URISyntaxException {
     ClassPathEntry cpe1mock = getClassPathEntry1AsMock();
     ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
@@ -255,6 +284,18 @@
   }
 
   /**
+   * @param classPathEntry
+   * @param string
+   */
+  private void assertJarEntry(ClassPathEntry classPathEntry, String expected) {
+    assertTrue("Should be instance of ZipFileClassPathEntry",
+        classPathEntry instanceof ZipFileClassPathEntry);
+    ZipFileClassPathEntry zipCPE = (ZipFileClassPathEntry) classPathEntry;
+    String jar = zipCPE.getLocation();
+    assertTrue("URL should contain " + expected, jar.contains(expected));
+  }
+
+  /**
    * Creates an array of class path entries, setting up each one with a
    * well-known set of client prefixes.
    */