Add support for the <type>/<package>:<name> method of declaring resources.

RELNOTES: None
PiperOrigin-RevId: 166899690
diff --git a/src/tools/android/java/com/google/devtools/build/android/DataValueFileWithIds.java b/src/tools/android/java/com/google/devtools/build/android/DataValueFileWithIds.java
index 4cbb1b1..052ec41 100644
--- a/src/tools/android/java/com/google/devtools/build/android/DataValueFileWithIds.java
+++ b/src/tools/android/java/com/google/devtools/build/android/DataValueFileWithIds.java
@@ -14,7 +14,6 @@
 package com.google.devtools.build.android;
 
 import com.android.SdkConstants;
-import com.android.resources.ResourceType;
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer;
 import com.google.devtools.build.android.xml.IdXmlResourceValue;
@@ -32,8 +31,8 @@
 import javax.xml.stream.events.XMLEvent;
 
 /**
- * Parses an XML file for "@+id/foo" and creates {@link IdXmlResourceValue} from parsed IDs.
- * This can be a layout file, menu, drawable, etc.
+ * Parses an XML file for "@+id/foo" and creates {@link IdXmlResourceValue} from parsed IDs. This
+ * can be a layout file, menu, drawable, etc.
  */
 public class DataValueFileWithIds {
 
@@ -79,13 +78,11 @@
     } catch (RuntimeException e) {
       throw new RuntimeException("Error parsing " + source, e);
     }
-    ImmutableSet<String> idResources = newIds.build();
     overwritingConsumer.consume(fileKey, DataValueFile.of(source));
-    for (String id : idResources) {
+    for (String id : newIds.build()) {
       combiningConsumer.consume(
-          fqnFactory.create(ResourceType.ID, id),
+          fqnFactory.parse("id/" + id),
           DataResourceXml.createWithNoNamespace(source, IdXmlResourceValue.of()));
     }
   }
-
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java b/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java
index 9d0575e..9344412 100644
--- a/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java
+++ b/src/tools/android/java/com/google/devtools/build/android/FullyQualifiedName.java
@@ -45,30 +45,27 @@
 /**
  * Represents a fully qualified name for an android resource.
  *
- * Each resource name consists of the resource package, name, type, and qualifiers.
+ * <p>Each resource name consists of the resource package, name, type, and qualifiers.
  */
 @Immutable
 public class FullyQualifiedName implements DataKey {
-  /** Represents the type of a {@link FullyQualifiedName}. */
-  public interface Type {
-    /**
-     * The category of type that a {@link Type} can be.
-     *
-     * <p>
-     * <em>Note:</em> used for strict ordering of {@link FullyQualifiedName}s.
-     */
-    public enum ConcreteType {
-      RESOURCE_TYPE,
-      VIRTUAL_TYPE;
-    }
+  public static final String DEFAULT_PACKAGE = "res-auto";
+  private static final Joiner DASH_JOINER = Joiner.on('-');
+  // To save on memory, always return one instance for each FullyQualifiedName.
+  // Using a HashMap to deduplicate the instances -- the key retrieves a single instance.
+  private static final ConcurrentMap<FullyQualifiedName, FullyQualifiedName> instanceCache =
+      new ConcurrentHashMap<>();
+  private static final AtomicInteger cacheHit = new AtomicInteger(0);
+  private final String pkg;
+  private final ImmutableList<String> qualifiers;
+  private final Type type;
+  private final String name;
 
-    public String getName();
-    public ConcreteType getType();
-    public boolean isOverwritable(FullyQualifiedName fqn);
-    public int compareTo(Type other);
-    @Override public boolean equals(Object obj);
-    @Override public int hashCode();
-    @Override public String toString();
+  private FullyQualifiedName(String pkg, ImmutableList<String> qualifiers, Type type, String name) {
+    this.pkg = pkg;
+    this.qualifiers = qualifiers;
+    this.type = type;
+    this.name = name;
   }
 
   private static Type createTypeFrom(String rawType) {
@@ -82,323 +79,6 @@
     return null;
   }
 
-  private static class ResourceTypeWrapper implements Type {
-    private final ResourceType resourceType;
-
-    public ResourceTypeWrapper(ResourceType resourceType) {
-      this.resourceType = resourceType;
-    }
-
-    @Override
-    public String getName() {
-      return resourceType.getName();
-    }
-
-    @Override
-    public ConcreteType getType() {
-      return ConcreteType.RESOURCE_TYPE;
-    }
-
-    @Override
-    public boolean isOverwritable(FullyQualifiedName fqn) {
-      return !(resourceType == ResourceType.ID
-          || resourceType == ResourceType.PUBLIC
-          || resourceType == ResourceType.STYLEABLE);
-    }
-
-    @Override
-    public int compareTo(Type other) {
-      if (!(other instanceof ResourceTypeWrapper)) {
-        return getType().compareTo(other.getType());
-      }
-      return resourceType.compareTo(((ResourceTypeWrapper) other).resourceType);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      if (!(obj instanceof ResourceTypeWrapper)) {
-        return false;
-      }
-      ResourceTypeWrapper other = (ResourceTypeWrapper) obj;
-      return Objects.equals(resourceType, other.resourceType);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hashCode(resourceType);
-    }
-
-    @Override
-    public String toString() {
-      return resourceType.toString();
-    }
-  }
-
-  /** The non-resource {@link Type}s of a {@link FullyQualifiedName}. */
-  public enum VirtualType implements Type {
-    RESOURCES_ATTRIBUTE("<resources>", "Resources Attribute");
-
-    /** Returns the enum represented by the {@code name}. */
-    public static VirtualType getEnum(String name) {
-      for (VirtualType type : values()) {
-        if (type.name.equals(name)) {
-          return type;
-        }
-      }
-      return null;
-    }
-
-    /** Returns an array with all the names defined by this enum. */
-    public static String[] getNames() {
-        VirtualType[] values = values();
-        String[] names = new String[values.length];
-        for (int i = values.length - 1; i >= 0; --i) {
-            names[i] = values[i].getName();
-        }
-        return names;
-    }
-
-    private final String name;
-    private final String displayName;
-
-    private VirtualType(String name, String displayName) {
-      this.name = name;
-      this.displayName = displayName;
-    }
-
-    /** Returns the resource type name. */
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    /** Returns a translated display name for the resource type. */
-    public String getDisplayName() {
-        return displayName;
-    }
-
-    @Override
-    public ConcreteType getType() {
-      return ConcreteType.VIRTUAL_TYPE;
-    }
-
-    @Override
-    public boolean isOverwritable(FullyQualifiedName fqn) {
-      if (this == RESOURCES_ATTRIBUTE) {
-        return !ResourcesAttribute.AttributeType.from(fqn.name()).isCombining();
-      }
-      return true;
-    }
-
-    @Override
-    public int compareTo(Type other) {
-      if (!(other instanceof VirtualType)) {
-        return getType().compareTo(other.getType());
-      }
-      return compareTo(((VirtualType) other));
-    }
-
-    @Override
-    public String toString() {
-        return getName();
-    }
-  }
-
-  public static final String DEFAULT_PACKAGE = "res-auto";
-  private static final Joiner DASH_JOINER = Joiner.on('-');
-
-  // To save on memory, always return one instance for each FullyQualifiedName.
-  // Using a HashMap to deduplicate the instances -- the key retrieves a single instance.
-  private static final ConcurrentMap<FullyQualifiedName, FullyQualifiedName> instanceCache =
-      new ConcurrentHashMap<>();
-  private static final AtomicInteger cacheHit = new AtomicInteger(0);
-
-  /**
-   * A factory for parsing an generating FullyQualified names with qualifiers and package.
-   */
-  public static class Factory {
-
-    private static final Pattern PARSING_REGEX =
-        Pattern.compile("(?:(?<package>[^:]+):){0,1}(?<type>[^-/]+)(?:[^/]*)/(?<name>.+)");
-    public static final String INVALID_QUALIFIED_NAME_MESSAGE_NO_MATCH =
-        String.format(
-            "%%s is not a valid qualified name. "
-                + "It should be in the pattern [package:]{%s}/name",
-            Joiner.on(",").join(ImmutableList.<String>builder()
-                .add(ResourceType.getNames())
-                .add(VirtualType.getNames())
-                .build()));
-    public static final String INVALID_QUALIFIED_NAME_MESSAGE_NO_TYPE_OR_NAME =
-        String.format(
-            "Could not find either resource type (%%s) or name (%%s) in %%s. "
-                + "It should be in the pattern [package:]{%s}/name",
-            Joiner.on(",").join(ImmutableList.<String>builder()
-                .add(ResourceType.getNames())
-                .add(VirtualType.getNames())
-                .build()));
-    public static final String INVALID_QUALIFIERS = "%s contains invalid qualifiers.";
-    private final ImmutableList<String> qualifiers;
-    private final String pkg;
-
-    private Factory(ImmutableList<String> qualifiers, String pkg) {
-      this.qualifiers = qualifiers;
-      this.pkg = pkg;
-    }
-
-    /** Creates a factory with default package from a directory name split on '-'. */
-    public static Factory fromDirectoryName(String[] dirNameAndQualifiers) {
-      return from(getQualifiers(dirNameAndQualifiers));
-    }
-
-    private static List<String> getQualifiers(String[] dirNameAndQualifiers) {
-      PeekingIterator<String> rawQualifiers =
-          Iterators.peekingIterator(Iterators.forArray(dirNameAndQualifiers));
-      // Remove directory name
-      rawQualifiers.next();
-      List<String> transformedLocaleQualifiers = new ArrayList<>();
-      List<String> handledQualifiers = new ArrayList<>();
-      // Do some substitution of language/region qualifiers.
-      while (rawQualifiers.hasNext()) {
-        String qualifier = rawQualifiers.next();
-        if ("es".equalsIgnoreCase(qualifier)
-            && rawQualifiers.hasNext()
-            && "419".equalsIgnoreCase(rawQualifiers.peek())) {
-          // Replace the es-419.
-          transformedLocaleQualifiers.add("b+es+419");
-          // Consume the next value, as it's been replaced.
-          rawQualifiers.next();
-        } else if ("sr".equalsIgnoreCase(qualifier)
-            && rawQualifiers.hasNext()
-            && "rlatn".equalsIgnoreCase(rawQualifiers.peek())) {
-          // Replace the sr-rLatn.
-          transformedLocaleQualifiers.add("b+sr+Latn");
-          // Consume the next value, as it's been replaced.
-          rawQualifiers.next();
-        } else {
-          // This qualifier can probably be handled by FolderConfiguration.
-          handledQualifiers.add(qualifier);
-        }
-      }
-      // Create a configuration
-      FolderConfiguration config = FolderConfiguration.getConfigFromQualifiers(handledQualifiers);
-      // FolderConfiguration returns an unhelpful null when it considers the qualifiers to be
-      // invalid.
-      if (config == null) {
-        throw new IllegalArgumentException(
-            String.format(INVALID_QUALIFIERS, DASH_JOINER.join(dirNameAndQualifiers)));
-      }
-      config.normalize();
-
-      // This is fragile but better than the Gradle scheme of just dropping
-      // entire subtrees.
-      Builder<String> builder = ImmutableList.<String>builder();
-      addIfNotNull(config.getCountryCodeQualifier(), builder);
-      addIfNotNull(config.getNetworkCodeQualifier(), builder);
-      if (transformedLocaleQualifiers.isEmpty()) {
-        addIfNotNull(config.getLocaleQualifier(), builder);
-      } else {
-        builder.addAll(transformedLocaleQualifiers);
-      }
-      // index 3 is past the country code, network code, and locale indices.
-      for (int i = 3; i < FolderConfiguration.getQualifierCount(); ++i) {
-        addIfNotNull(config.getQualifier(i), builder);
-      }
-      return builder.build();
-    }
-
-    private static void addIfNotNull(
-        ResourceQualifier qualifier, ImmutableList.Builder<String> builder) {
-      if (qualifier != null) {
-        builder.add(qualifier.getFolderSegment());
-      }
-    }
-
-    public static Factory from(List<String> qualifiers, String pkg) {
-      return new Factory(ImmutableList.copyOf(qualifiers), pkg);
-    }
-
-    public static Factory from(List<String> qualifiers) {
-      return from(ImmutableList.copyOf(qualifiers), DEFAULT_PACKAGE);
-    }
-
-    public FullyQualifiedName create(Type type, String name, String pkg) {
-      return FullyQualifiedName.of(pkg, qualifiers, type, name);
-    }
-
-    public FullyQualifiedName create(ResourceType type, String name) {
-      return create(new ResourceTypeWrapper(type), name, pkg);
-    }
-
-    public FullyQualifiedName create(VirtualType type, String name) {
-      return create(type, name, pkg);
-    }
-
-    /**
-     * Parses a FullyQualifiedName from a string.
-     *
-     * @param raw A string in the expected format from
-     *     [&lt;package&gt;:]&lt;ResourceType.name&gt;/&lt;resource name&gt;.
-     * @throws IllegalArgumentException when the raw string is not valid qualified name.
-     */
-    public FullyQualifiedName parse(String raw) {
-      Matcher matcher = PARSING_REGEX.matcher(raw);
-      if (!matcher.matches()) {
-        throw new IllegalArgumentException(
-            String.format(INVALID_QUALIFIED_NAME_MESSAGE_NO_MATCH, raw));
-      }
-      String parsedPackage = matcher.group("package");
-      Type type = createTypeFrom(matcher.group("type"));
-      String name = matcher.group("name");
-
-      if (type == null || name == null) {
-        throw new IllegalArgumentException(
-            String.format(
-                INVALID_QUALIFIED_NAME_MESSAGE_NO_TYPE_OR_NAME, type, name, raw));
-      }
-      return FullyQualifiedName.of(
-          parsedPackage == null ? pkg : parsedPackage, qualifiers, type, name);
-    }
-
-    /**
-     * Generates a FullyQualifiedName for a file-based resource given the source Path.
-     *
-     * @param sourcePath the path of the file-based resource.
-     * @throws IllegalArgumentException if the file-based resource has an invalid filename
-     */
-    public FullyQualifiedName parse(Path sourcePath) {
-      return parse(deriveRawFullyQualifiedName(sourcePath));
-    }
-
-    private static String deriveRawFullyQualifiedName(Path source) {
-      if (source.getNameCount() < 2) {
-        throw new IllegalArgumentException(
-            String.format(
-                "The resource path %s is too short. "
-                    + "The path is expected to be <resource type>/<file name>.",
-                source));
-      }
-      // Compose the `pathWithExtension` manually to ensure it uses a forward slash.
-      // Using Path.subpath would return a backslash-using path on Windows.
-      String pathWithExtension = source.getParent().getFileName() + "/" + source.getFileName();
-      int extensionStart = pathWithExtension.indexOf('.');
-      if (extensionStart > 0) {
-        return pathWithExtension.substring(0, extensionStart);
-      }
-      return pathWithExtension;
-    }
-
-    // Grabs the extension portion of the path removed by deriveRawFullyQualifiedName.
-    private static String getSourceExtension(Path source) {
-      // TODO(corysmith): Find out if there is a filename parser utility.
-      String fileName = source.getFileName().toString();
-      int extensionStart = fileName.indexOf('.');
-      if (extensionStart > 0) {
-        return fileName.substring(extensionStart);
-      }
-      return "";
-    }
-  }
-
   /**
    * Creates a new FullyQualifiedName with normalized qualifiers.
    *
@@ -408,8 +88,7 @@
    * @param name The name of the name.
    * @return A new FullyQualifiedName.
    */
-  public static FullyQualifiedName of(
-      String pkg, List<String> qualifiers, Type type, String name) {
+  public static FullyQualifiedName of(String pkg, List<String> qualifiers, Type type, String name) {
     checkNotNull(pkg);
     checkNotNull(qualifiers);
     checkNotNull(type);
@@ -417,8 +96,7 @@
     ImmutableList<String> immutableQualifiers = ImmutableList.copyOf(qualifiers);
     // TODO(corysmith): Address the GC thrash this creates by managing a simplified, mutable key to
     // do the instance check.
-    FullyQualifiedName fqn =
-        new FullyQualifiedName(pkg, immutableQualifiers, type, name);
+    FullyQualifiedName fqn = new FullyQualifiedName(pkg, immutableQualifiers, type, name);
     // Use putIfAbsent to get the canonical instance, if there. If it isn't, putIfAbsent will
     // return null, and we should return the current instance.
     FullyQualifiedName cached = instanceCache.putIfAbsent(fqn, fqn);
@@ -456,31 +134,14 @@
     logger.fine(
         String.format(
             "Total FullyQualifiedName instance cache hits %s out of %s",
-            cacheHit.intValue(),
-            instanceCache.size()));
-  }
-
-  private final String pkg;
-  private final ImmutableList<String> qualifiers;
-  private final Type type;
-  private final String name;
-
-  private FullyQualifiedName(
-      String pkg,
-      ImmutableList<String> qualifiers,
-      Type type,
-      String name) {
-    this.pkg = pkg;
-    this.qualifiers = qualifiers;
-    this.type = type;
-    this.name = name;
+            cacheHit.intValue(), instanceCache.size()));
   }
 
   /**
    * Returns a string path representation of the FullyQualifiedName.
    *
-   * Non-values Android Resource have a well defined file layout: From the resource directory, they
-   * reside in &lt;resource type&gt;[-&lt;qualifier&gt;]/&lt;resource name&gt;[.extension]
+   * <p>Non-values Android Resource have a well defined file layout: From the resource directory,
+   * they reside in &lt;resource type&gt;[-&lt;qualifier&gt;]/&lt;resource name&gt;[.extension]
    *
    * @param source The original source of the file-based resource's FullyQualifiedName
    * @return A string representation of the FullyQualifiedName with the provided extension.
@@ -489,10 +150,7 @@
     String sourceExtension = FullyQualifiedName.Factory.getSourceExtension(source);
     return Paths.get(
             DASH_JOINER.join(
-                ImmutableList.<String>builder()
-                    .add(type.getName())
-                    .addAll(qualifiers)
-                    .build()),
+                ImmutableList.<String>builder().add(type.getName()).addAll(qualifiers).build()),
             name + sourceExtension)
         .toString();
   }
@@ -510,7 +168,7 @@
   /**
    * Returns the string path representation of the values directory and qualifiers.
    *
-   * Certain resource types live in the "values" directory. This will calculate the directory and
+   * <p>Certain resource types live in the "values" directory. This will calculate the directory and
    * ensure the qualifiers are represented.
    */
   // TODO(corysmith): Combine this with toPathString to clean up the interface of FullyQualifiedName
@@ -618,4 +276,363 @@
         .addAllQualifiers(qualifiers)
         .setKeyValue(name);
   }
+
+  /** The non-resource {@link Type}s of a {@link FullyQualifiedName}. */
+  public enum VirtualType implements Type {
+    RESOURCES_ATTRIBUTE("<resources>", "Resources Attribute");
+
+    private final String name;
+    private final String displayName;
+
+    private VirtualType(String name, String displayName) {
+      this.name = name;
+      this.displayName = displayName;
+    }
+
+    /** Returns the enum represented by the {@code name}. */
+    public static VirtualType getEnum(String name) {
+      for (VirtualType type : values()) {
+        if (type.name.equals(name)) {
+          return type;
+        }
+      }
+      return null;
+    }
+
+    /** Returns an array with all the names defined by this enum. */
+    public static String[] getNames() {
+      VirtualType[] values = values();
+      String[] names = new String[values.length];
+      for (int i = values.length - 1; i >= 0; --i) {
+        names[i] = values[i].getName();
+      }
+      return names;
+    }
+
+    /** Returns the resource type name. */
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    /** Returns a translated display name for the resource type. */
+    public String getDisplayName() {
+      return displayName;
+    }
+
+    @Override
+    public ConcreteType getType() {
+      return ConcreteType.VIRTUAL_TYPE;
+    }
+
+    @Override
+    public boolean isOverwritable(FullyQualifiedName fqn) {
+      if (this == RESOURCES_ATTRIBUTE) {
+        return !ResourcesAttribute.AttributeType.from(fqn.name()).isCombining();
+      }
+      return true;
+    }
+
+    @Override
+    public int compareTo(Type other) {
+      if (!(other instanceof VirtualType)) {
+        return getType().compareTo(other.getType());
+      }
+      return compareTo(((VirtualType) other));
+    }
+
+    @Override
+    public String toString() {
+      return getName();
+    }
+  }
+
+  /** Represents the type of a {@link FullyQualifiedName}. */
+  public interface Type {
+    public String getName();
+
+    public ConcreteType getType();
+
+    public boolean isOverwritable(FullyQualifiedName fqn);
+
+    public int compareTo(Type other);
+
+    @Override
+    public boolean equals(Object obj);
+
+    @Override
+    public int hashCode();
+
+    @Override
+    public String toString();
+
+    /**
+     * The category of type that a {@link Type} can be.
+     *
+     * <p><em>Note:</em> used for strict ordering of {@link FullyQualifiedName}s.
+     */
+    public enum ConcreteType {
+      RESOURCE_TYPE,
+      VIRTUAL_TYPE;
+    }
+  }
+
+  private static class ResourceTypeWrapper implements Type {
+    private final ResourceType resourceType;
+
+    public ResourceTypeWrapper(ResourceType resourceType) {
+      this.resourceType = resourceType;
+    }
+
+    @Override
+    public String getName() {
+      return resourceType.getName();
+    }
+
+    @Override
+    public ConcreteType getType() {
+      return ConcreteType.RESOURCE_TYPE;
+    }
+
+    @Override
+    public boolean isOverwritable(FullyQualifiedName fqn) {
+      return !(resourceType == ResourceType.ID
+          || resourceType == ResourceType.PUBLIC
+          || resourceType == ResourceType.STYLEABLE);
+    }
+
+    @Override
+    public int compareTo(Type other) {
+      if (!(other instanceof ResourceTypeWrapper)) {
+        return getType().compareTo(other.getType());
+      }
+      return resourceType.compareTo(((ResourceTypeWrapper) other).resourceType);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof ResourceTypeWrapper)) {
+        return false;
+      }
+      ResourceTypeWrapper other = (ResourceTypeWrapper) obj;
+      return Objects.equals(resourceType, other.resourceType);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(resourceType);
+    }
+
+    @Override
+    public String toString() {
+      return resourceType.toString();
+    }
+  }
+
+  /** A factory for parsing an generating FullyQualified names with qualifiers and package. */
+  public static class Factory {
+
+    public static final String INVALID_QUALIFIED_NAME_MESSAGE_NO_MATCH =
+        String.format(
+            "%%s is not a valid qualified name. "
+                + "It should be in the pattern [package:]{%s}/name",
+            Joiner.on(",")
+                .join(
+                    ImmutableList.<String>builder()
+                        .add(ResourceType.getNames())
+                        .add(VirtualType.getNames())
+                        .build()));
+    public static final String INVALID_QUALIFIED_NAME_MESSAGE_NO_TYPE_OR_NAME =
+        String.format(
+            "Could not find either resource type (%%s) or name (%%s) in %%s. "
+                + "It should be in the pattern [package:]{%s}/name",
+            Joiner.on(",")
+                .join(
+                    ImmutableList.<String>builder()
+                        .add(ResourceType.getNames())
+                        .add(VirtualType.getNames())
+                        .build()));
+    public static final String INVALID_QUALIFIERS = "%s contains invalid qualifiers.";
+    private static final Pattern PARSING_REGEX =
+        Pattern.compile(
+            "(?:(?<package>[^:]+):){0,1}(?<type>[^-/]+)(?:[^/]*)/(?:(?:(?<namespace>\\{[^}]+\\}))"
+                + "|(?:(?<misplacedPackage>[^:]+):)){0,1}(?<name>.+)");
+    private final ImmutableList<String> qualifiers;
+    private final String pkg;
+
+    private Factory(ImmutableList<String> qualifiers, String pkg) {
+      this.qualifiers = qualifiers;
+      this.pkg = pkg;
+    }
+
+    /** Creates a factory with default package from a directory name split on '-'. */
+    public static Factory fromDirectoryName(String[] dirNameAndQualifiers) {
+      return from(getQualifiers(dirNameAndQualifiers));
+    }
+
+    private static List<String> getQualifiers(String[] dirNameAndQualifiers) {
+      PeekingIterator<String> rawQualifiers =
+          Iterators.peekingIterator(Iterators.forArray(dirNameAndQualifiers));
+      // Remove directory name
+      rawQualifiers.next();
+      List<String> transformedLocaleQualifiers = new ArrayList<>();
+      List<String> handledQualifiers = new ArrayList<>();
+      // Do some substitution of language/region qualifiers.
+      while (rawQualifiers.hasNext()) {
+        String qualifier = rawQualifiers.next();
+        if ("es".equalsIgnoreCase(qualifier)
+            && rawQualifiers.hasNext()
+            && "419".equalsIgnoreCase(rawQualifiers.peek())) {
+          // Replace the es-419.
+          transformedLocaleQualifiers.add("b+es+419");
+          // Consume the next value, as it's been replaced.
+          rawQualifiers.next();
+        } else if ("sr".equalsIgnoreCase(qualifier)
+            && rawQualifiers.hasNext()
+            && "rlatn".equalsIgnoreCase(rawQualifiers.peek())) {
+          // Replace the sr-rLatn.
+          transformedLocaleQualifiers.add("b+sr+Latn");
+          // Consume the next value, as it's been replaced.
+          rawQualifiers.next();
+        } else {
+          // This qualifier can probably be handled by FolderConfiguration.
+          handledQualifiers.add(qualifier);
+        }
+      }
+      // Create a configuration
+      FolderConfiguration config = FolderConfiguration.getConfigFromQualifiers(handledQualifiers);
+      // FolderConfiguration returns an unhelpful null when it considers the qualifiers to be
+      // invalid.
+      if (config == null) {
+        throw new IllegalArgumentException(
+            String.format(INVALID_QUALIFIERS, DASH_JOINER.join(dirNameAndQualifiers)));
+      }
+      config.normalize();
+
+      // This is fragile but better than the Gradle scheme of just dropping
+      // entire subtrees.
+      Builder<String> builder = ImmutableList.<String>builder();
+      addIfNotNull(config.getCountryCodeQualifier(), builder);
+      addIfNotNull(config.getNetworkCodeQualifier(), builder);
+      if (transformedLocaleQualifiers.isEmpty()) {
+        addIfNotNull(config.getLocaleQualifier(), builder);
+      } else {
+        builder.addAll(transformedLocaleQualifiers);
+      }
+      // index 3 is past the country code, network code, and locale indices.
+      for (int i = 3; i < FolderConfiguration.getQualifierCount(); ++i) {
+        addIfNotNull(config.getQualifier(i), builder);
+      }
+      return builder.build();
+    }
+
+    private static void addIfNotNull(
+        ResourceQualifier qualifier, ImmutableList.Builder<String> builder) {
+      if (qualifier != null) {
+        builder.add(qualifier.getFolderSegment());
+      }
+    }
+
+    public static Factory from(List<String> qualifiers, String pkg) {
+      return new Factory(ImmutableList.copyOf(qualifiers), pkg);
+    }
+
+    public static Factory from(List<String> qualifiers) {
+      return from(ImmutableList.copyOf(qualifiers), DEFAULT_PACKAGE);
+    }
+
+    private static String deriveRawFullyQualifiedName(Path source) {
+      if (source.getNameCount() < 2) {
+        throw new IllegalArgumentException(
+            String.format(
+                "The resource path %s is too short. "
+                    + "The path is expected to be <resource type>/<file name>.",
+                source));
+      }
+      // Compose the `pathWithExtension` manually to ensure it uses a forward slash.
+      // Using Path.subpath would return a backslash-using path on Windows.
+      String pathWithExtension = source.getParent().getFileName() + "/" + source.getFileName();
+      int extensionStart = pathWithExtension.indexOf('.');
+      if (extensionStart > 0) {
+        return pathWithExtension.substring(0, extensionStart);
+      }
+      return pathWithExtension;
+    }
+
+    // Grabs the extension portion of the path removed by deriveRawFullyQualifiedName.
+    private static String getSourceExtension(Path source) {
+      // TODO(corysmith): Find out if there is a filename parser utility.
+      String fileName = source.getFileName().toString();
+      int extensionStart = fileName.indexOf('.');
+      if (extensionStart > 0) {
+        return fileName.substring(extensionStart);
+      }
+      return "";
+    }
+
+    public FullyQualifiedName create(Type type, String name, String pkg) {
+      return FullyQualifiedName.of(pkg, qualifiers, type, name);
+    }
+
+    public FullyQualifiedName create(ResourceType type, String name) {
+      return create(new ResourceTypeWrapper(type), name, pkg);
+    }
+
+    public FullyQualifiedName create(ResourceType type, String name, String pkg) {
+      return create(new ResourceTypeWrapper(type), name, pkg);
+    }
+
+    public FullyQualifiedName create(VirtualType type, String name) {
+      return create(type, name, pkg);
+    }
+
+    /**
+     * Parses a FullyQualifiedName from a string.
+     *
+     * @param raw A string in the expected format from
+     *     [&lt;package&gt;:]&lt;ResourceType.name&gt;/&lt;resource name&gt;.
+     * @throws IllegalArgumentException when the raw string is not valid qualified name.
+     */
+    public FullyQualifiedName parse(String raw) {
+      Matcher matcher = PARSING_REGEX.matcher(raw);
+      if (!matcher.matches()) {
+        throw new IllegalArgumentException(
+            String.format(INVALID_QUALIFIED_NAME_MESSAGE_NO_MATCH, raw));
+      }
+      String parsedPackage =
+          firstNonNull(matcher.group("package"), matcher.group("misplacedPackage"), pkg);
+
+      Type type = createTypeFrom(matcher.group("type"));
+      String name =
+          matcher.group("namespace") != null
+              ? matcher.group("namespace") + matcher.group("name")
+              : matcher.group("name");
+
+      if (type == null || name == null) {
+        throw new IllegalArgumentException(
+            String.format(INVALID_QUALIFIED_NAME_MESSAGE_NO_TYPE_OR_NAME, type, name, raw));
+      }
+
+      return FullyQualifiedName.of(parsedPackage, qualifiers, type, name);
+    }
+
+    private String firstNonNull(String... values) {
+      for (String value : values) {
+        if (value != null) {
+          return value;
+        }
+      }
+      throw new NullPointerException("Expected a nonnull value.");
+    }
+
+    /**
+     * Generates a FullyQualifiedName for a file-based resource given the source Path.
+     *
+     * @param sourcePath the path of the file-based resource.
+     * @throws IllegalArgumentException if the file-based resource has an invalid filename
+     */
+    public FullyQualifiedName parse(Path sourcePath) {
+      return parse(deriveRawFullyQualifiedName(sourcePath));
+    }
+  }
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java b/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java
index 60cbd1f..3e4d200 100644
--- a/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java
+++ b/src/tools/android/java/com/google/devtools/build/android/XmlResourceValues.java
@@ -178,12 +178,16 @@
   static XmlResourceValue parseId(
       XMLEventReader eventReader, StartElement start, Namespaces.Collector namespacesCollector)
       throws XMLStreamException {
-    if (XmlResourceValues.isEndTag(eventReader.peek(), start.getName())) {
-      return IdXmlResourceValue.of();
-    } else {
-      return IdXmlResourceValue.of(
-          readContentsAsString(
-              eventReader, start.getName(), namespacesCollector.collectFrom(start)));
+    try {
+      if (XmlResourceValues.isEndTag(eventReader.peek(), start.getName())) {
+        return IdXmlResourceValue.of();
+      } else {
+        return IdXmlResourceValue.of(
+            readContentsAsString(
+                eventReader, start.getName(), namespacesCollector.collectFrom(start)));
+      }
+    } catch (IllegalArgumentException e) {
+      throw new XMLStreamException(e);
     }
   }
 
diff --git a/src/tools/android/java/com/google/devtools/build/android/resources/RClassGenerator.java b/src/tools/android/java/com/google/devtools/build/android/resources/RClassGenerator.java
index 81a55e0..e170cd7 100644
--- a/src/tools/android/java/com/google/devtools/build/android/resources/RClassGenerator.java
+++ b/src/tools/android/java/com/google/devtools/build/android/resources/RClassGenerator.java
@@ -17,6 +17,7 @@
 
 import com.android.SdkConstants;
 import com.android.resources.ResourceType;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
@@ -142,6 +143,12 @@
     }
     for (Entry<String, FieldInitializer> entry : initializers.entrySet()) {
       FieldInitializer init = entry.getValue();
+      Preconditions.checkArgument(
+          !entry.getKey().contains(":"),
+          "%s in %s, %s is invalid java id",
+          entry.getKey(),
+          packageDir,
+          fullyQualifiedInnerClass);
       if (init.writeFieldDefinition(
           entry.getKey(), innerClassWriter, fieldAccessLevel, finalFields)) {
         deferredInitializers.put(entry.getKey(), init);