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
- * [<package>:]<ResourceType.name>/<resource name>.
- * @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 <resource type>[-<qualifier>]/<resource name>[.extension]
+ * <p>Non-values Android Resource have a well defined file layout: From the resource directory,
+ * they reside in <resource type>[-<qualifier>]/<resource name>[.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
+ * [<package>:]<ResourceType.name>/<resource name>.
+ * @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);