Introduce DexJavaLibraryIfItContainsClassFiles Buildable.

Summary:
`DexJavaLibraryIfItContainsClassFiles` is a `Buildable` that takes the output of a `JavaLibraryRule` and
and dexes it, assuming the output of the `JavaLibraryRule` contains `.class` files.
It relies on an `AccumulateClassNames` to determine whether there are `.class` files to dex.

Once `DexJavaLibraryIfItContainsClassFiles` has the path to the classes file,
it uses a `FileExistsAndIsNotEmptyStep` combined with a `ConditionalStep` to
make sure that the list of `.class` files is non-empty before running the `DxStep`
because `dx` will fail if it is run on input that does not contain any `.class` files.

Test Plan: Sandcastle builds.
diff --git a/src/com/facebook/buck/android/BUCK b/src/com/facebook/buck/android/BUCK
index ef50c93..7be1768 100644
--- a/src/com/facebook/buck/android/BUCK
+++ b/src/com/facebook/buck/android/BUCK
@@ -55,6 +55,7 @@
   'AndroidTransitiveDependencyGraph.java',
   'ApkGenrule.java',
   'ApkGenruleBuildRuleFactory.java',
+  'DexProducedFromJavaLibraryThatContainsClassFiles.java',
   'GenAidl.java',
   'GenAidlBuildRuleFactory.java',
   'HasAndroidPlatformTarget.java',
diff --git a/src/com/facebook/buck/android/DexProducedFromJavaLibraryThatContainsClassFiles.java b/src/com/facebook/buck/android/DexProducedFromJavaLibraryThatContainsClassFiles.java
new file mode 100644
index 0000000..0073cc6
--- /dev/null
+++ b/src/com/facebook/buck/android/DexProducedFromJavaLibraryThatContainsClassFiles.java
@@ -0,0 +1,190 @@
+/*
+ * 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.android;
+
+import com.facebook.buck.java.AccumulateClassNames;
+import com.facebook.buck.java.JavaLibraryRule;
+import com.facebook.buck.model.BuildTarget;
+import com.facebook.buck.rules.AbstractBuildRuleBuilderParams;
+import com.facebook.buck.rules.AbstractBuildable;
+import com.facebook.buck.rules.BuildContext;
+import com.facebook.buck.rules.BuildRuleParams;
+import com.facebook.buck.rules.BuildRuleResolver;
+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.AbstractExecutionStep;
+import com.facebook.buck.step.CompositeStep;
+import com.facebook.buck.step.ConditionalStep;
+import com.facebook.buck.step.ExecutionContext;
+import com.facebook.buck.step.Step;
+import com.facebook.buck.step.fs.FileExistsAndIsNotEmptyStep;
+import com.facebook.buck.step.fs.MkdirStep;
+import com.facebook.buck.step.fs.RmStep;
+import com.facebook.buck.util.BuckConstant;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * {@link DexProducedFromJavaLibraryThatContainsClassFiles} is a {@link Buildable} that serves a
+ * very specific purpose: it takes a {@link JavaLibraryRule} and the list of classes in the
+ * {@link JavaLibraryRule} (which is represented by an {@link AccumulateClassNames}), and dexes the
+ * output of the {@link JavaLibraryRule} if its list of classes is non-empty. Because it is
+ * expected to be used with pre-dexing, we always pass the {@code --force-jumbo} flag to {@code dx}
+ * in this buildable.
+ * <p>
+ * Most {@link Buildable}s can determine the (possibly null) path to their output file from their
+ * definition. This is an anomaly because we do not know whether this will write a {@code .dex} file
+ * until runtime. Unfortunately, because there is no such thing as an empty {@code .dex} file, we
+ * cannot write a meaningful "dummy .dex" if there are no class files to pass to {@code dx}.
+ */
+public class DexProducedFromJavaLibraryThatContainsClassFiles extends AbstractBuildable {
+
+  /**
+   * Key used with {@link OnDiskBuildInfo} to identify whether this {@link Buildable} has
+   * generated a {@code .dex} files. The only expected value to be associated with this key is
+   * {@code "true"}.
+   */
+  private static final String HAS_DEX_OUTPUT_METADATA = "HAS_DEX_OUTPUT";
+
+  private final BuildTarget buildTarget;
+  private final AccumulateClassNames javaLibraryWithClassesList;
+
+  /** This {@link Supplier} will be defined and determined after this buildable is built. */
+  @Nullable
+  private Supplier<Boolean> hasOutputFile;
+
+  private DexProducedFromJavaLibraryThatContainsClassFiles(BuildTarget buildTarget,
+      AccumulateClassNames javaLibraryWithClassesList) {
+    this.buildTarget = Preconditions.checkNotNull(buildTarget);
+    this.javaLibraryWithClassesList = Preconditions.checkNotNull(javaLibraryWithClassesList);
+  }
+
+  @Override
+  public Iterable<String> getInputsToCompareToOutput() {
+    // The deps of this rule already capture all of the inputs that should affect the cache key.
+    return ImmutableList.of();
+  }
+
+  @Override
+  public List<Step> getBuildSteps(BuildContext context, final BuildableContext buildableContext)
+      throws IOException {
+    ImmutableList.Builder<Step> steps = ImmutableList.builder();
+
+    steps.add(new RmStep(getPathToDex().toString(), /* shouldForceDeletion */ true));
+
+    // Make sure that the buck-out/gen/ directory exists for this.buildTarget.
+    steps.add(new MkdirStep(getPathToDex().getParent()));
+
+    // Check whether the list of class files in the JavaLibraryRule is empty.
+    FileExistsAndIsNotEmptyStep fileExistsStep = new FileExistsAndIsNotEmptyStep(
+        Paths.get(javaLibraryWithClassesList.getPathToOutputFile()));
+    hasOutputFile = fileExistsStep;
+    steps.add(fileExistsStep);
+
+    // To be conservative, use --force-jumbo for these intermediate .dex files so that they can be
+    // merged into a final classes.dex that uses jumbo instructions.
+    JavaLibraryRule javaLibraryRuleToDex = javaLibraryWithClassesList.getJavaLibraryRule();
+    DxStep dx = new DxStep(getPathToDex().toString(),
+        Collections.singleton(Paths.get(javaLibraryRuleToDex.getPathToOutputFile())),
+        EnumSet.of(DxStep.Option.NO_OPTIMIZE, DxStep.Option.FORCE_JUMBO));
+    AbstractExecutionStep recordArtifactStep = new AbstractExecutionStep("record dx success") {
+      @Override
+      public int execute(ExecutionContext context) {
+        buildableContext.recordArtifact(getPathToDex().getFileName());
+        buildableContext.addMetadata(HAS_DEX_OUTPUT_METADATA, "true");
+        return 0;
+      }
+    };
+    CompositeStep dxAndStore = new CompositeStep(ImmutableList.of(dx, recordArtifactStep));
+
+    // Make sure that there are .class files to dex before running dx.
+    ConditionalStep runDxIfThereAreClassFiles = new ConditionalStep(fileExistsStep, dxAndStore);
+    steps.add(runDxIfThereAreClassFiles);
+
+    return steps.build();
+  }
+
+  @Override
+  @Nullable
+  public String getPathToOutputFile() {
+    // A .dex file is not guaranteed to be generated, so we return null to be conservative.
+    return null;
+  }
+
+  public Path getPathToDex() {
+    return Paths.get(
+        BuckConstant.GEN_DIR,
+        buildTarget.getBasePath(),
+        buildTarget.getShortName() + ".dex.jar");
+  }
+
+  public boolean hasOutput() {
+    // 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(hasOutputFile);
+    return hasOutputFile.get();
+  }
+
+  @Override
+  protected void initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
+    boolean hasOutput = "true".equals(onDiskBuildInfo.getValue(HAS_DEX_OUTPUT_METADATA).orNull());
+    this.hasOutputFile = Suppliers.ofInstance(hasOutput);
+  }
+
+  public static Builder newPreDexBuilder(AbstractBuildRuleBuilderParams params) {
+    return new Builder(params);
+  }
+
+  public static class Builder extends AbstractBuildable.Builder {
+
+    private AccumulateClassNames javaLibraryWithClassesList;
+
+    private Builder(AbstractBuildRuleBuilderParams params) {
+      super(params);
+    }
+
+    @Override
+    protected BuildRuleType getType() {
+      return BuildRuleType._PRE_DEX;
+    }
+
+    @Override
+    protected DexProducedFromJavaLibraryThatContainsClassFiles newBuildable(
+        BuildRuleParams params, BuildRuleResolver resolver) {
+      return new DexProducedFromJavaLibraryThatContainsClassFiles(params.getBuildTarget(),
+          javaLibraryWithClassesList);
+    }
+
+    public Builder setPathToClassNamesList(AccumulateClassNames javaLibraryWithClassesList) {
+      this.javaLibraryWithClassesList = javaLibraryWithClassesList;
+      return this;
+    }
+  }
+}
diff --git a/src/com/facebook/buck/java/AccumulateClassNames.java b/src/com/facebook/buck/java/AccumulateClassNames.java
index 880012a..84bfced 100644
--- a/src/com/facebook/buck/java/AccumulateClassNames.java
+++ b/src/com/facebook/buck/java/AccumulateClassNames.java
@@ -67,6 +67,10 @@
     return pathToOutputFile.toString();
   }
 
+  public JavaLibraryRule getJavaLibraryRule() {
+    return javaLibraryRule;
+  }
+
   @Override
   public List<Step> getBuildSteps(BuildContext context, BuildableContext buildableContext)
       throws IOException {
diff --git a/src/com/facebook/buck/rules/AbstractBuildable.java b/src/com/facebook/buck/rules/AbstractBuildable.java
index 696662b..de9fc1b 100644
--- a/src/com/facebook/buck/rules/AbstractBuildable.java
+++ b/src/com/facebook/buck/rules/AbstractBuildable.java
@@ -39,6 +39,14 @@
     return builder;
   }
 
+  /**
+   * @param onDiskBuildInfo Contains metadata that was read from disk.
+   * @see AbstractCachingBuildRule#initializeFromDisk
+   */
+  protected void initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
+
+  }
+
   protected static abstract class Builder extends AbstractBuildRuleBuilder<AbstractCachingBuildRule> {
 
     protected Builder(AbstractBuildRuleBuilderParams params) {
@@ -65,6 +73,13 @@
         public BuildRuleType getType() {
           return type;
         }
+
+        @Override
+        protected void initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
+          if (buildable instanceof AbstractBuildable) {
+            ((AbstractBuildable) buildable).initializeFromDisk(onDiskBuildInfo);
+          }
+        }
       };
     }
   }
diff --git a/src/com/facebook/buck/rules/BuildRuleType.java b/src/com/facebook/buck/rules/BuildRuleType.java
index 385eba5..5995d23 100644
--- a/src/com/facebook/buck/rules/BuildRuleType.java
+++ b/src/com/facebook/buck/rules/BuildRuleType.java
@@ -46,6 +46,7 @@
 
   // Internal rule types. Denoted by leading trailing underscore.
   public static BuildRuleType _CLASS_NAMES = new BuildRuleType("_class_names");
+  public static BuildRuleType _PRE_DEX = new BuildRuleType("_pre_dex");
 
   private final String name;
   private final boolean isTestRule;
diff --git a/src/com/facebook/buck/step/fs/FileExistsAndIsNotEmptyStep.java b/src/com/facebook/buck/step/fs/FileExistsAndIsNotEmptyStep.java
new file mode 100644
index 0000000..9a72c01
--- /dev/null
+++ b/src/com/facebook/buck/step/fs/FileExistsAndIsNotEmptyStep.java
@@ -0,0 +1,68 @@
+/*
+ * 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.step.fs;
+
+import com.facebook.buck.step.AbstractExecutionStep;
+import com.facebook.buck.step.ConditionalStep;
+import com.facebook.buck.step.ExecutionContext;
+import com.facebook.buck.util.ProjectFilesystem;
+import com.facebook.buck.util.TriState;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+/**
+ * Step that verifies that a file of non-zero length exists at the specified path. The side effect
+ * of this step is that its {@link #get()} method will return a boolean indicating whether the
+ * file exists. This is designed to be used with a {@link ConditionalStep}.
+ */
+public class FileExistsAndIsNotEmptyStep extends AbstractExecutionStep
+    implements Supplier<Boolean> {
+
+  private final Path pathToFile;
+  private TriState fileExistsAndIsNotEmpty = TriState.UNSPECIFIED;
+
+  public FileExistsAndIsNotEmptyStep(Path pathToFile) {
+    super("test -s " + pathToFile);
+    this.pathToFile = Preconditions.checkNotNull(pathToFile);
+  }
+
+  @Override
+  public Boolean get() {
+    Preconditions.checkState(fileExistsAndIsNotEmpty.isSet());
+    return fileExistsAndIsNotEmpty.asBoolean();
+  }
+
+  @Override
+  public int execute(ExecutionContext context) {
+    ProjectFilesystem filesystem = context.getProjectFilesystem();
+    boolean exists = filesystem.exists(pathToFile.toString());
+
+    try {
+      fileExistsAndIsNotEmpty = TriState.forBooleanValue(exists
+          && filesystem.getFileSize(pathToFile) > 0);
+    } catch (IOException e) {
+      context.logError(e, "Failed to get size for file: %s.", pathToFile);
+      return 1;
+    }
+
+    return 0;
+  }
+
+}