AccumulateClassNames makes the discovered class names and their hashes available.

Summary:
In turn, this makes the class names and hashes available to
`DexProducedFromJavaLibraryThatContainsClassFiles`, which is important for:
(1) Avoiding a re-dex when the .class files have not changed.
(2) Making it possible for split-dex to determine which intermediate .dex
files need to be in the primary classes.dex.

Test Plan: Sandcastle builds.
diff --git a/src/com/facebook/buck/java/AccumulateClassNames.java b/src/com/facebook/buck/java/AccumulateClassNames.java
index 84bfced..142bfad 100644
--- a/src/com/facebook/buck/java/AccumulateClassNames.java
+++ b/src/com/facebook/buck/java/AccumulateClassNames.java
@@ -25,29 +25,50 @@
 import com.facebook.buck.rules.BuildRuleType;
 import com.facebook.buck.rules.Buildable;
 import com.facebook.buck.rules.BuildableContext;
+import com.facebook.buck.rules.OnDiskBuildInfo;
 import com.facebook.buck.step.Step;
 import com.facebook.buck.step.fs.MkdirStep;
 import com.facebook.buck.step.fs.RmStep;
 import com.facebook.buck.util.BuckConstant;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.hash.HashCode;
 
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * {@link Buildable} that writes the list of {@code .class} files found in a zip or directory to a
  * file.
  */
 public class AccumulateClassNames extends AbstractBuildable {
 
+  private static final Splitter CLASS_NAME_AND_HASH_SPLITTER = Splitter.on(
+      AccumulateClassNamesStep.CLASS_NAME_HASH_CODE_SEPARATOR);
+
   private final JavaLibraryRule javaLibraryRule;
   private final Path pathToOutputFile;
 
-  private AccumulateClassNames(BuildTarget buildTarget, JavaLibraryRule javaLibraryRule) {
+  /**
+   * This will contain the classes info discovered in the course of building this {@link Buildable}.
+   * This {@link Supplier} will be defined and determined after this buildable is built.
+   */
+  @VisibleForTesting
+  @Nullable
+  Supplier<ImmutableSortedMap<String, HashCode>> classNames;
+
+  @VisibleForTesting
+  AccumulateClassNames(BuildTarget buildTarget, JavaLibraryRule javaLibraryRule) {
     Preconditions.checkNotNull(buildTarget);
     this.javaLibraryRule = Preconditions.checkNotNull(javaLibraryRule);
     this.pathToOutputFile = Paths.get(
@@ -81,13 +102,47 @@
     // Make sure that the output directory exists for the output file.
     steps.add(new MkdirStep(pathToOutputFile.getParent()));
 
-    steps.add(new AccumulateClassNamesStep(
+    AccumulateClassNamesStep accumulateClassNamesStep = new AccumulateClassNamesStep(
         Paths.get(javaLibraryRule.getPathToOutputFile()),
-        Paths.get(getPathToOutputFile())));
+        Paths.get(getPathToOutputFile()));
+    classNames = accumulateClassNamesStep;
+    steps.add(accumulateClassNamesStep);
 
     return steps.build();
   }
 
+  public ImmutableSortedMap<String, HashCode> getClassNames() {
+    // TODO(mbolin): Assert that this Buildable has been built. Currently, there is no way to do
+    // that from a Buildable (but there is from an AbstractCachingBuildRule).
+    Preconditions.checkNotNull(classNames);
+    return classNames.get();
+  }
+
+  @Override
+  protected void initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
+    // Assign this.classNames when instantiating this Buildable when pulled from cache.
+
+    // Read the output file, which should now be in place because this rule was downloaded from
+    // cache.
+    List<String> lines;
+    try {
+      lines = onDiskBuildInfo.getOutputFileContentsByLine(this);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    // Use the contents of the file to create the ImmutableSortedMap<String, HashCode>.
+    ImmutableSortedMap.Builder<String, HashCode> classNamesBuilder = ImmutableSortedMap.naturalOrder();
+    for (String line : lines) {
+      List<String> parts = CLASS_NAME_AND_HASH_SPLITTER.splitToList(line);
+      Preconditions.checkState(parts.size() == 2);
+      String key = parts.get(0);
+      HashCode value = HashCode.fromString(parts.get(1));
+      classNamesBuilder.put(key, value);
+    }
+    this.classNames = Suppliers.ofInstance(classNamesBuilder.build());
+  }
+
   public static Builder newAccumulateClassNamesBuilder(AbstractBuildRuleBuilderParams params) {
     return new Builder(params);
   }
diff --git a/src/com/facebook/buck/java/AccumulateClassNamesStep.java b/src/com/facebook/buck/java/AccumulateClassNamesStep.java
index fd1ccbc..e2e0cb5 100644
--- a/src/com/facebook/buck/java/AccumulateClassNamesStep.java
+++ b/src/com/facebook/buck/java/AccumulateClassNamesStep.java
@@ -25,6 +25,7 @@
 import com.facebook.buck.step.Step;
 import com.google.common.base.Function;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
 import com.google.common.hash.HashCode;
@@ -39,11 +40,14 @@
 import java.util.Map;
 import java.util.Map.Entry;
 
+import javax.annotation.Nullable;
+
 /**
  * {@link Step} that takes a directory or zip of {@code .class} files and traverses it to get the
  * total set of {@code .class} files included by the directory or zip.
  */
-public class AccumulateClassNamesStep extends AbstractExecutionStep {
+public class AccumulateClassNamesStep extends AbstractExecutionStep
+    implements Supplier<ImmutableSortedMap<String, HashCode>> {
 
   /**
    * In the generated {@code classes.txt} file, each line will contain the path to a {@code .class}
@@ -61,6 +65,14 @@
   private final Path pathToJarOrClassesDirectory;
   private final Path whereClassNamesShouldBeWritten;
 
+  /**
+   * Map of names of {@code .class} files to hashes of their contents.
+   * <p>
+   * This is not set until this step is executed.
+   */
+  @Nullable
+  private ImmutableSortedMap<String, HashCode> classNames;
+
   public AccumulateClassNamesStep(Path pathToJarOrClassesDirectory,
       Path whereClassNamesShouldBeWritten) {
     super("get_class_names " + pathToJarOrClassesDirectory + " > " + whereClassNamesShouldBeWritten);
@@ -119,7 +131,15 @@
       return 1;
     }
 
+    this.classNames = classNames;
     return 0;
   }
 
+  @Override
+  public ImmutableSortedMap<String, HashCode> get() {
+    Preconditions.checkNotNull(classNames,
+        "Step must be executed successfully before invoking this method.");
+    return classNames;
+  }
+
 }
diff --git a/src/com/facebook/buck/rules/OnDiskBuildInfo.java b/src/com/facebook/buck/rules/OnDiskBuildInfo.java
index 06555d5..cc84282 100644
--- a/src/com/facebook/buck/rules/OnDiskBuildInfo.java
+++ b/src/com/facebook/buck/rules/OnDiskBuildInfo.java
@@ -21,7 +21,10 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
+import java.io.IOException;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
 
 /**
  * Utility for reading the metadata associated with a build rule's output. This is metadata that
@@ -73,4 +76,16 @@
     return getValue(BuildInfo.METADATA_KEY_FOR_RULE_KEY_WITHOUT_DEPS)
         .transform(RuleKey.TO_RULE_KEY);
   }
+
+  /**
+   * Invokes the {@link Buildable#getPathToOutputFile()} method of the specified {@link Buildable},
+   * reads the file at the specified path, and returns the list of lines in the file.
+   */
+  public List<String> getOutputFileContentsByLine(Buildable buildable) throws IOException {
+    Preconditions.checkNotNull(buildable);
+    String pathToOutputFile = buildable.getPathToOutputFile();
+    Preconditions.checkNotNull(pathToOutputFile);
+    Path path = Paths.get(pathToOutputFile);
+    return projectFilesystem.readLines(path);
+  }
 }
diff --git a/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java b/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
index 60de63a..3512cd0 100644
--- a/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
+++ b/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
@@ -23,6 +23,8 @@
 import com.facebook.buck.util.ProjectFilesystem;
 import com.google.common.base.Charsets;
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.hash.HashCode;
 import com.google.common.io.Files;
 
 import org.junit.Rule;
@@ -80,6 +82,14 @@
             "com/example/Foo" + separator + SHA1_FOR_EMPTY_STRING,
             "com/example/subpackage/Baz" + separator + SHA1_FOR_EMPTY_STRING) + '\n',
         contents);
+    assertEquals(
+        "get() should return the class name/hash mappings.",
+        ImmutableSortedMap.of(
+          "com/example/Bar", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+          "com/example/Foo", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+          "com/example/subpackage/Baz", HashCode.fromString(SHA1_FOR_EMPTY_STRING)
+        ),
+        accumulateClassNamesStep.get());
   }
 
   @Test
@@ -115,5 +125,21 @@
             "com/example/Foo" + separator + SHA1_FOR_EMPTY_STRING,
             "com/example/subpackage/Baz" + separator + SHA1_FOR_EMPTY_STRING) + '\n',
         contents);
+    assertEquals(
+        "get() should return the class name/hash mappings.",
+        ImmutableSortedMap.of(
+          "com/example/Bar", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+          "com/example/Foo", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+          "com/example/subpackage/Baz", HashCode.fromString(SHA1_FOR_EMPTY_STRING)
+        ),
+        accumulateClassNamesStep.get());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testGetClassesBeforeExecuteThrowsException() {
+    AccumulateClassNamesStep accumulateClassNamesStep = new AccumulateClassNamesStep(
+        Paths.get("pathToJarOrClassesDirectory"),
+        Paths.get("whereClassNamesShouldBeWritten"));
+    accumulateClassNamesStep.get();
   }
 }
diff --git a/test/com/facebook/buck/java/AccumulateClassNamesTest.java b/test/com/facebook/buck/java/AccumulateClassNamesTest.java
index f04c5d2..d9987a6 100644
--- a/test/com/facebook/buck/java/AccumulateClassNamesTest.java
+++ b/test/com/facebook/buck/java/AccumulateClassNamesTest.java
@@ -16,7 +16,9 @@
 
 package com.facebook.buck.java;
 
+import static org.easymock.EasyMock.expect;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 import com.facebook.buck.model.BuildTarget;
 import com.facebook.buck.rules.AbstractBuildRuleBuilderParams;
@@ -26,13 +28,16 @@
 import com.facebook.buck.rules.BuildRuleType;
 import com.facebook.buck.rules.BuildableContext;
 import com.facebook.buck.rules.FakeAbstractBuildRuleBuilderParams;
+import com.facebook.buck.rules.OnDiskBuildInfo;
 import com.facebook.buck.step.ExecutionContext;
 import com.facebook.buck.step.Step;
 import com.facebook.buck.step.TestExecutionContext;
 import com.facebook.buck.testutil.MoreAsserts;
 import com.facebook.buck.util.ProjectFilesystem;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.hash.HashCode;
 
 import org.easymock.EasyMockSupport;
 import org.junit.Test;
@@ -85,6 +90,9 @@
     List<Step> steps = accumulateClassNames.getBuildSteps(buildContext, buildableContext);
     verifyAll();
 
+    assertNotNull("The Supplier should be set as a side-effect of creating the steps.",
+        accumulateClassNames.classNames);
+
     // Verify the build steps.
     ProjectFilesystem projectFilesystem = new ProjectFilesystem(new File("."));
     ExecutionContext context = TestExecutionContext
@@ -100,4 +108,36 @@
         steps,
         context);
   }
+
+  @Test
+  public void testInitializeFromDisk() throws IOException {
+    BuildTarget buildTarget = new BuildTarget("//foo", "bar");
+    JavaLibraryRule javaRule = createMock(JavaLibraryRule.class);
+
+    replayAll();
+    AccumulateClassNames accumulateClassNames = new AccumulateClassNames(buildTarget, javaRule);
+    verifyAll();
+    resetAll();
+
+    OnDiskBuildInfo onDiskBuildInfo = createMock(OnDiskBuildInfo.class);
+    List<String> lines = ImmutableList.of(
+        "com/example/Bar 087b7707a5f8e0a2adf5652e3cd2072d89a197dc",
+        "com/example/Baz 62b1c2510840c0de55c13f66065a98a719be0f19",
+        "com/example/Foo e4fccb7520b7795e632651323c63217c9f59f72a");
+    expect(onDiskBuildInfo.getOutputFileContentsByLine(accumulateClassNames)).andReturn(lines);
+
+    replayAll();
+    accumulateClassNames.initializeFromDisk(onDiskBuildInfo);
+    verifyAll();
+
+    ImmutableSortedMap<String, HashCode> observedClasses = accumulateClassNames.getClassNames();
+    assertEquals(
+        "initializeFromDisk() should read the lines and use them to create an ImmutableSortedMap.",
+        ImmutableSortedMap.of(
+          "com/example/Bar", HashCode.fromString("087b7707a5f8e0a2adf5652e3cd2072d89a197dc"),
+          "com/example/Baz", HashCode.fromString("62b1c2510840c0de55c13f66065a98a719be0f19"),
+          "com/example/Foo", HashCode.fromString("e4fccb7520b7795e632651323c63217c9f59f72a")
+        ),
+        observedClasses);
+  }
 }
diff --git a/test/com/facebook/buck/rules/OnDiskBuildInfoTest.java b/test/com/facebook/buck/rules/OnDiskBuildInfoTest.java
new file mode 100644
index 0000000..6c9d0ee
--- /dev/null
+++ b/test/com/facebook/buck/rules/OnDiskBuildInfoTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2013-present Facebook, 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.facebook.buck.rules;
+
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+
+import com.facebook.buck.model.BuildTarget;
+import com.facebook.buck.util.ProjectFilesystem;
+import com.google.common.collect.ImmutableList;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+
+public class OnDiskBuildInfoTest extends EasyMockSupport {
+
+  @Test
+  public void testGetOutputFileContentsByLine() throws IOException {
+    String pathToOutputFile = "buck-out/gen/java/com/example/classes.txt";
+
+    ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
+    List<String> lines = ImmutableList.of(
+        "com/example/Bar.class 087b7707a5f8e0a2adf5652e3cd2072d89a197dc",
+        "com/example/Foo.class e4fccb7520b7795e632651323c63217c9f59f72a");
+    expect(projectFilesystem.readLines(Paths.get(pathToOutputFile))).andReturn(lines);
+
+    Buildable buildable = createMock(Buildable.class);
+    expect(buildable.getPathToOutputFile()).andReturn(pathToOutputFile);
+
+    replayAll();
+
+    OnDiskBuildInfo onDiskBuildInfo = new OnDiskBuildInfo(
+        new BuildTarget("//java/com/example", "ex"),
+        projectFilesystem);
+    List<String> observedLines = onDiskBuildInfo.getOutputFileContentsByLine(buildable);
+    assertEquals(lines, observedLines);
+
+    verifyAll();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testGetOutputFileContentsByLineRejectsNullBuildable() throws IOException {
+    ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
+    OnDiskBuildInfo onDiskBuildInfo = new OnDiskBuildInfo(
+        new BuildTarget("//java/com/example", "ex"),
+        projectFilesystem);
+
+    replayAll();
+
+    onDiskBuildInfo.getOutputFileContentsByLine(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testGetOutputFileContentsByLineRejectsNullOutputPath() throws IOException {
+    ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
+    OnDiskBuildInfo onDiskBuildInfo = new OnDiskBuildInfo(
+        new BuildTarget("//java/com/example", "ex"),
+        projectFilesystem);
+
+    Buildable buildable = createMock(Buildable.class);
+    expect(buildable.getPathToOutputFile()).andReturn(null);
+
+    replayAll();
+
+    onDiskBuildInfo.getOutputFileContentsByLine(buildable);
+  }
+
+}