Avoid to re-index classpath and bootclasspath

Avoid to re-index classpath and bootclasspath for each input/output pair since they share the same classpath and bootclasspath.
RELNOTES: n/a

--
PiperOrigin-RevId: 151015798
MOS_MIGRATED_REVID=151015798

GitOrigin-RevId: e182500db1f331fd39f7e5d6215e6fdedbb0791a
Change-Id: I4b201e163d25403e1217f1a4422d643e153e0c99
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 22780ea..1711f57 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -26,6 +26,7 @@
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsParser;
+import com.google.errorprone.annotations.MustBeClosed;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.FileVisitResult;
@@ -193,124 +194,137 @@
 
     LambdaClassMaker lambdas = new LambdaClassMaker(dumpDirectory);
 
-    // Process each input separately
-    for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) {
-      Path inputPath = inputOutputPair.getInput();
-      Path outputPath = inputOutputPair.getOutput();
-      checkState(
-          Files.isDirectory(inputPath) || !Files.isDirectory(outputPath),
-          "Input jar file requires an output jar file");
-      
-      try (Closer closer = Closer.create();
-          OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath)) {
-        InputFileProvider appInputFiles = toInputFileProvider(closer, inputPath);
-        List<InputFileProvider> classpathInputFiles =
-            toInputFileProvider(closer, options.classpath);
-        IndexedInputs appIndexedInputs = new IndexedInputs(ImmutableList.of(appInputFiles));
-        IndexedInputs appAndClasspathIndexedInputs =
-            new IndexedInputs(classpathInputFiles, appIndexedInputs);
-        ClassLoader loader =
-            createClassLoader(
-                rewriter,
-                toInputFileProvider(closer, options.bootclasspath),
-                appAndClasspathIndexedInputs);
+    try (Closer closer = Closer.create()) {
+      IndexedInputs indexedClasspath =
+          new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
+      // Use a classloader that as much as possible uses the provided bootclasspath instead of
+      // the tool's system classloader.  Unfortunately we can't do that for java. classes.
+      ClassLoader bootclassloader =
+          options.bootclasspath.isEmpty()
+              ? new ThrowingClassLoader()
+              : new HeaderClassLoader(
+                  new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)),
+                  rewriter,
+                  new ThrowingClassLoader());
 
-        ClassReaderFactory readerFactory =
-            new ClassReaderFactory(
-                (options.copyBridgesFromClasspath && !allowDefaultMethods)
-                    ? appAndClasspathIndexedInputs
-                    : appIndexedInputs,
-                rewriter);
+      // Process each input separately
+      for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) {
+        Path inputPath = inputOutputPair.getInput();
+        Path outputPath = inputOutputPair.getOutput();
+        checkState(
+            Files.isDirectory(inputPath) || !Files.isDirectory(outputPath),
+            "Input jar file requires an output jar file");
 
-        ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
+        try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath);
+            InputFileProvider inputFiles = toInputFileProvider(inputPath)) {
+          IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles));
+          // Prepend classpath with input file itself so LambdaDesugaring can load classes with
+          // lambdas.
+          IndexedInputs indexedClasspathAndInputFiles =
+              indexedClasspath.withParent(indexedInputFiles);
+          // Note that input file and classpath need to be in the same classloader because
+          // we typically get the header Jar for inputJar on the classpath and having the header
+          // Jar in a parent loader means the header version is preferred over the real thing.
+          ClassLoader loader =
+              new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader);
 
-        // Process inputs, desugaring as we go
-        for (String filename : appInputFiles) {
-          try (InputStream content = appInputFiles.getInputStream(filename)) {
-            // We can write classes uncompressed since they need to be converted to .dex format
-            // for Android anyways. Resources are written as they were in the input jar to avoid
-            // any danger of accidentally uncompressed resources ending up in an .apk.
-            if (filename.endsWith(".class")) {
-              ClassReader reader = rewriter.reader(content);
-              CoreLibraryRewriter.UnprefixingClassWriter writer =
-                  rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/);
-              ClassVisitor visitor = writer;
+          ClassReaderFactory readerFactory =
+              new ClassReaderFactory(
+                  (options.copyBridgesFromClasspath && !allowDefaultMethods)
+                      ? indexedClasspathAndInputFiles
+                      : indexedInputFiles,
+                  rewriter);
 
-              if (!options.onlyDesugarJavac9ForLint) {
-                if (!allowDefaultMethods) {
-                  visitor = new Java7Compatibility(visitor, readerFactory);
+          ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
+
+          // Process inputs, desugaring as we go
+          for (String filename : inputFiles) {
+            try (InputStream content = inputFiles.getInputStream(filename)) {
+              // We can write classes uncompressed since they need to be converted to .dex format
+              // for Android anyways. Resources are written as they were in the input jar to avoid
+              // any danger of accidentally uncompressed resources ending up in an .apk.
+              if (filename.endsWith(".class")) {
+                ClassReader reader = rewriter.reader(content);
+                CoreLibraryRewriter.UnprefixingClassWriter writer =
+                    rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/);
+                ClassVisitor visitor = writer;
+
+                if (!options.onlyDesugarJavac9ForLint) {
+                  if (!allowDefaultMethods) {
+                    visitor = new Java7Compatibility(visitor, readerFactory);
+                  }
+
+                  visitor =
+                      new LambdaDesugaring(
+                          visitor,
+                          loader,
+                          lambdas,
+                          interfaceLambdaMethodCollector,
+                          allowDefaultMethods);
                 }
 
-                visitor =
-                    new LambdaDesugaring(
-                        visitor,
-                        loader,
-                        lambdas,
-                        interfaceLambdaMethodCollector,
-                        allowDefaultMethods);
+                if (!allowCallsToObjectsNonNull) {
+                  visitor = new ObjectsRequireNonNullMethodInliner(visitor);
+                }
+                reader.accept(visitor, 0);
+
+                outputFileProvider.write(filename, writer.toByteArray());
+              } else {
+                outputFileProvider.copyFrom(filename, inputFiles);
+              }
+            }
+          }
+
+          ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build();
+          checkState(
+              !allowDefaultMethods || interfaceLambdaMethods.isEmpty(),
+              "Desugaring with default methods enabled moved interface lambdas");
+
+            // Write out the lambda classes we generated along the way
+            ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain();
+            checkState(
+                !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(),
+                "There should be no lambda classes generated: %s",
+                lambdaClasses.keySet());
+
+            for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) {
+              try (InputStream bytecode =
+                  Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) {
+                ClassReader reader = rewriter.reader(bytecode);
+                CoreLibraryRewriter.UnprefixingClassWriter writer =
+                    rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/);
+                ClassVisitor visitor = writer;
+
+              if (!allowDefaultMethods) {
+                // null ClassReaderFactory b/c we don't expect to need it for lambda classes
+                visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null);
               }
 
+              visitor =
+                  new LambdaClassFixer(
+                      visitor,
+                      lambdaClass.getValue(),
+                      readerFactory,
+                      interfaceLambdaMethods,
+                      allowDefaultMethods);
+              // Send lambda classes through desugaring to make sure there's no invokedynamic
+              // instructions in generated lambda classes (checkState below will fail)
+              visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods);
               if (!allowCallsToObjectsNonNull) {
+                // Not sure whether there will be implicit null check emitted by javac, so we rerun
+                // the inliner again
                 visitor = new ObjectsRequireNonNullMethodInliner(visitor);
               }
               reader.accept(visitor, 0);
-
+              String filename =
+                  rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
               outputFileProvider.write(filename, writer.toByteArray());
-            } else {
-              outputFileProvider.copyFrom(filename, appInputFiles);
             }
           }
+
+          Map<Path, LambdaInfo> leftBehind = lambdas.drain();
+          checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind);
         }
-
-        ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build();
-        checkState(
-            !allowDefaultMethods || interfaceLambdaMethods.isEmpty(),
-            "Desugaring with default methods enabled moved interface lambdas");
-
-        // Write out the lambda classes we generated along the way
-        ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain();
-        checkState(
-            !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(),
-            "There should be no lambda classes generated: %s",
-            lambdaClasses.keySet());
-
-        for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) {
-          try (InputStream bytecode =
-              Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) {
-            ClassReader reader = rewriter.reader(bytecode);
-            CoreLibraryRewriter.UnprefixingClassWriter writer =
-                rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/);
-            ClassVisitor visitor = writer;
-
-            if (!allowDefaultMethods) {
-              // null ClassReaderFactory b/c we don't expect to need it for lambda classes
-              visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null);
-            }
-
-            visitor =
-                new LambdaClassFixer(
-                    visitor,
-                    lambdaClass.getValue(),
-                    readerFactory,
-                    interfaceLambdaMethods,
-                    allowDefaultMethods);
-            // Send lambda classes through desugaring to make sure there's no invokedynamic
-            // instructions in generated lambda classes (checkState below will fail)
-            visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods);
-            if (!allowCallsToObjectsNonNull) {
-              // Not sure whether there will be implicit null check emitted by javac, so we rerun
-              // the inliner again
-              visitor = new ObjectsRequireNonNullMethodInliner(visitor);
-            }
-            reader.accept(visitor, 0);
-            String filename =
-                rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
-            outputFileProvider.write(filename, writer.toByteArray());
-          }
-        }
-
-        Map<Path, LambdaInfo> leftBehind = lambdas.drain();
-        checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind);
       }
     }
   }
@@ -325,24 +339,6 @@
     return ioPairListbuilder.build();
   }
 
-  private static ClassLoader createClassLoader(
-      CoreLibraryRewriter rewriter,
-      List<InputFileProvider> bootclasspath,
-      IndexedInputs appAndClasspathIndexedInputs)
-      throws IOException {
-    // Use a classloader that as much as possible uses the provided bootclasspath instead of
-    // the tool's system classloader.  Unfortunately we can't do that for java. classes.
-    ClassLoader parent = new ThrowingClassLoader();
-    if (!bootclasspath.isEmpty()) {
-      parent = new HeaderClassLoader(new IndexedInputs(bootclasspath), rewriter, parent);
-    }
-    // Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas.
-    // Note that inputJar and classpath need to be in the same classloader because we typically get
-    // the header Jar for inputJar on the classpath and having the header Jar in a parent loader
-    // means the header version is preferred over the real thing.
-    return new HeaderClassLoader(appAndClasspathIndexedInputs, rewriter, parent);
-  }
-
   private static class ThrowingClassLoader extends ClassLoader {
     @Override
     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
@@ -394,6 +390,7 @@
   }
 
   /** Transform a Path to an {@link OutputFileProvider} */
+  @MustBeClosed
   private static OutputFileProvider toOutputFileProvider(Path path)
       throws IOException {
     if (Files.isDirectory(path)) {
@@ -403,17 +400,22 @@
     }
   }
 
-  /** Transform a Path to an InputFileProvider and register it to close it at the end of desugar */
-  private static InputFileProvider toInputFileProvider(Closer closer, Path path)
+  /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
+  @MustBeClosed
+  private static InputFileProvider toInputFileProvider(Path path)
       throws IOException {
     if (Files.isDirectory(path)) {
-      return closer.register(new DirectoryInputFileProvider(path));
+      return new DirectoryInputFileProvider(path);
     } else {
-      return closer.register(new ZipInputFileProvider(path));
+      return new ZipInputFileProvider(path);
     }
   }
 
-  private static ImmutableList<InputFileProvider> toInputFileProvider(
+  /**
+   * Transform a list of Path to a list of ZipInputFileProvider and register them with the given
+   * closer.
+   */
+  private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider(
       Closer closer, List<Path> paths) throws IOException {
     ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
     for (Path path : paths) {
@@ -422,7 +424,7 @@
     }
     return builder.build();
   }
-  
+
   /**
    * Pair input and output.
    */
diff --git a/java/com/google/devtools/build/android/desugar/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/IndexedInputs.java
index 58459cc..33c6132 100644
--- a/java/com/google/devtools/build/android/desugar/IndexedInputs.java
+++ b/java/com/google/devtools/build/android/desugar/IndexedInputs.java
@@ -13,11 +13,14 @@
 // limitations under the License.
 package com.google.devtools.build.android.desugar;
 
-import com.google.common.base.Preconditions;
-import java.io.IOException;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import javax.annotation.CheckReturnValue;
 import javax.annotation.Nullable;
 
 /**
@@ -27,36 +30,47 @@
  */
 class IndexedInputs {
 
-  private final Map<String, InputFileProvider> inputFiles = new HashMap<>();
+  private final ImmutableMap<String, InputFileProvider> inputFiles;
 
-  /** Parent indexed inputs to use before to search a file name into this indexed inputs. */
+  /**
+   * Parent {@link IndexedInputs} to use before to search a file name into this {@link
+   * IndexedInputs}.
+   */
   @Nullable
-  private final IndexedInputs parentIndexedInputs;
+  private final IndexedInputs parent;
 
-  /** Index a list of input files without a parent indexed inputs. */
-  public IndexedInputs(List<InputFileProvider> inputProviders) throws IOException {
-    this(inputProviders, null);
+  /** Index a list of input files without a parent {@link IndexedInputs}. */
+  public IndexedInputs(List<InputFileProvider> inputProviders) {
+    this.parent = null;
+    this.inputFiles = indexInputs(inputProviders);
   }
 
   /**
-   * Index a list of input files and set a parent indexed inputs that is firstly used during the
-   * search of a file name.
+   * Create a new {@link IndexedInputs} with input files previously indexed and with a parent {@link
+   * IndexedInputs}.
    */
-  public IndexedInputs(
-      List<InputFileProvider> inputProviders, @Nullable IndexedInputs parentIndexedInputs)
-      throws IOException {
-    this.parentIndexedInputs = parentIndexedInputs;
-    for (InputFileProvider inputProvider : inputProviders) {
-      indexInput(inputProvider);
-    }
+  private IndexedInputs(
+      ImmutableMap<String, InputFileProvider> inputFiles, IndexedInputs parentIndexedInputs) {
+    this.parent = parentIndexedInputs;
+    this.inputFiles = inputFiles;
+  }
+
+  /**
+   * Create a new {@link IndexedInputs} with input files already indexed and with a parent {@link
+   * IndexedInputs}.
+   */
+  @CheckReturnValue
+  public IndexedInputs withParent(IndexedInputs parent) {
+    checkState(this.parent == null);
+    return new IndexedInputs(this.inputFiles, parent);
   }
 
   @Nullable
   public InputFileProvider getInputFileProvider(String filename) {
-    Preconditions.checkArgument(filename.endsWith(".class"));
+    checkArgument(filename.endsWith(".class"));
 
-    if (parentIndexedInputs != null) {
-      InputFileProvider inputFileProvider = parentIndexedInputs.getInputFileProvider(filename);
+    if (parent != null) {
+      InputFileProvider inputFileProvider = parent.getInputFileProvider(filename);
       if (inputFileProvider != null) {
         return inputFileProvider;
       }
@@ -65,11 +79,16 @@
     return inputFiles.get(filename);
   }
 
-  private void indexInput(final InputFileProvider inputFileProvider) throws IOException {
-    for (String relativePath : inputFileProvider) {
-      if (relativePath.endsWith(".class") && !inputFiles.containsKey(relativePath)) {
-        inputFiles.put(relativePath, inputFileProvider);
+  private ImmutableMap<String, InputFileProvider> indexInputs(
+      List<InputFileProvider> inputProviders) {
+    Map<String, InputFileProvider> indexedInputs = new HashMap<>();
+    for (InputFileProvider inputProvider : inputProviders) {
+      for (String relativePath : inputProvider) {
+        if (relativePath.endsWith(".class") && !indexedInputs.containsKey(relativePath)) {
+          indexedInputs.put(relativePath, inputProvider);
+        }
       }
     }
+    return ImmutableMap.copyOf(indexedInputs);
   }
 }