Introduce optional flavor projection to BuildTarget.

Summary:
For now, this will be reserved for synthetic build rules that
are inserted via graph-enhancement.

Test Plan: Sandcastle builds.
diff --git a/src/com/facebook/buck/model/BuildTarget.java b/src/com/facebook/buck/model/BuildTarget.java
index d7d4b58..56e4a9b 100644
--- a/src/com/facebook/buck/model/BuildTarget.java
+++ b/src/com/facebook/buck/model/BuildTarget.java
@@ -19,11 +19,16 @@
 import com.facebook.buck.util.BuckConstant;
 import com.facebook.buck.util.ProjectFilesystem;
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
 import java.io.File;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.regex.Pattern;
 
 import javax.annotation.Nullable;
 
@@ -34,22 +39,47 @@
 
   public static final String BUILD_TARGET_PREFIX = "//";
 
+  private static final Pattern VALID_FLAVOR_PATTERN = Pattern.compile("[a-zA-Z_]+");
+
   private final String baseName;
   private final String shortName;
+  private final Optional<String> flavor;
   private final String fullyQualifiedName;
 
   public BuildTarget(String baseName, String shortName) {
+    this(baseName, shortName, /* flavor */ Optional.<String>absent());
+  }
+
+  public BuildTarget(String baseName, String shortName, String flavor) {
+    this(baseName, shortName, Optional.of(flavor));
+  }
+
+  private BuildTarget(String baseName, String shortName, Optional<String> flavor) {
     Preconditions.checkNotNull(baseName);
+    // shortName may be the empty string when parsing visibility patterns.
+    Preconditions.checkNotNull(shortName);
+    Preconditions.checkNotNull(flavor);
+
     Preconditions.checkArgument(baseName.startsWith(BUILD_TARGET_PREFIX),
         "baseName must start with // but was %s",
         baseName);
 
-    // On Windows, baseName may contain backslashes, which are not permitted by BuildTarget.
-    baseName = baseName.replace("\\", "/");
+    Preconditions.checkArgument(!shortName.contains("#"),
+        "Build target name cannot contain '#' but was: %s.",
+        shortName);
+    if (flavor.isPresent()) {
+      String flavorName = flavor.get();
+      if (!VALID_FLAVOR_PATTERN.matcher(flavorName).matches()) {
+        throw new IllegalArgumentException("Invalid flavor: " + flavorName);
+      }
+      shortName += "#" + flavorName;
+    }
 
-    this.baseName = baseName;
-    this.shortName = Preconditions.checkNotNull(shortName);
-    this.fullyQualifiedName = String.format("%s:%s", baseName, shortName);
+    // On Windows, baseName may contain backslashes, which are not permitted by BuildTarget.
+    this.baseName = baseName.replace("\\", "/");
+    this.shortName = shortName;
+    this.flavor = flavor;
+    this.fullyQualifiedName = baseName + ":" + shortName;
   }
 
   /**
@@ -82,7 +112,7 @@
   }
 
   public Path getBuildFilePath() {
-    return java.nio.file.Paths.get(getBaseNameWithSlash() + BuckConstant.BUILD_RULES_FILE_NAME);
+    return Paths.get(getBaseNameWithSlash() + BuckConstant.BUILD_RULES_FILE_NAME);
   }
 
   /**
@@ -94,6 +124,15 @@
     return shortName;
   }
 
+  @VisibleForTesting
+  String getShortNameWithoutFlavor() {
+    if (!isFlavored()) {
+      return shortName;
+    } else {
+      return shortName.substring(0, shortName.length() - flavor.get().length() - 1);
+    }
+  }
+
   /**
    * If this build target were //third_party/java/guava:guava-latest, then this would return
    * "//third_party/java/guava".
@@ -149,6 +188,11 @@
     return fullyQualifiedName;
   }
 
+  @JsonIgnore
+  public boolean isFlavored() {
+    return flavor.isPresent();
+  }
+
   @Override
   public boolean equals(Object o) {
     if (!(o instanceof BuildTarget)) {
diff --git a/test/com/facebook/buck/model/BuildTargetTest.java b/test/com/facebook/buck/model/BuildTargetTest.java
index 0c29f0f..710a4a3 100644
--- a/test/com/facebook/buck/model/BuildTargetTest.java
+++ b/test/com/facebook/buck/model/BuildTargetTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -87,4 +88,40 @@
     BuildTarget ioTarget = new BuildTarget("//src/com/facebook/buck/util", "io");
     assertFalse(utilTarget.equals(ioTarget));
   }
+
+  @Test
+  public void testBuildTargetWithFlavor() {
+    BuildTarget target = new BuildTarget("//foo/bar", "baz", "dex");
+    assertEquals(target.getShortName(), "baz#dex");
+    assertEquals(target.getShortNameWithoutFlavor(), "baz");
+    assertTrue(target.isFlavored());
+  }
+
+  @Test
+  public void testBuildTargetWithoutFlavor() {
+    BuildTarget target = new BuildTarget("//foo/bar", "baz");
+    assertEquals(target.getShortName(), "baz");
+    assertEquals(target.getShortNameWithoutFlavor(), "baz");
+    assertFalse(target.isFlavored());
+  }
+
+  @Test
+  public void testFlavorIsValid() {
+    try {
+      new BuildTarget("//foo/bar", "baz", "d3x");
+      fail("Should have thrown IllegalArgumentException.");
+    } catch (IllegalArgumentException e) {
+      assertEquals("Invalid flavor: d3x", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testShortNameCannotContainHash() {
+    try {
+      new BuildTarget("//foo/bar", "baz#dex");
+      fail("Should have thrown IllegalArgumentException.");
+    } catch (IllegalArgumentException e) {
+      assertEquals("Build target name cannot contain '#' but was: baz#dex.", e.getMessage());
+    }
+  }
 }