|  | // Copyright 2016 The Bazel Authors. All rights reserved. | 
|  | // | 
|  | // 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.google.devtools.build.android; | 
|  |  | 
|  | import static com.google.common.base.Preconditions.checkNotNull; | 
|  | import static com.google.common.base.Strings.emptyToNull; | 
|  |  | 
|  | import com.android.SdkConstants; | 
|  | import com.android.annotations.VisibleForTesting; | 
|  | import com.android.ide.common.resources.configuration.FolderConfiguration; | 
|  | import com.android.ide.common.resources.configuration.ResourceQualifier; | 
|  | import com.android.resources.ResourceFolderType; | 
|  | import com.android.resources.ResourceType; | 
|  | import com.google.common.base.Ascii; | 
|  | import com.google.common.base.Joiner; | 
|  | import com.google.common.base.MoreObjects; | 
|  | import com.google.common.base.Preconditions; | 
|  | import com.google.common.base.Splitter; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.Iterators; | 
|  | import com.google.common.collect.PeekingIterator; | 
|  | import com.google.devtools.build.android.proto.SerializeFormat; | 
|  | import com.google.devtools.build.android.resources.ResourceTypeEnum; | 
|  | import com.google.devtools.build.android.xml.ResourcesAttribute; | 
|  | import com.google.errorprone.annotations.CheckReturnValue; | 
|  | import java.io.IOException; | 
|  | import java.io.OutputStream; | 
|  | import java.nio.file.Path; | 
|  | import java.nio.file.Paths; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.List; | 
|  | import java.util.Objects; | 
|  | import java.util.Optional; | 
|  | import java.util.concurrent.ConcurrentHashMap; | 
|  | import java.util.concurrent.ConcurrentMap; | 
|  | import java.util.concurrent.atomic.AtomicInteger; | 
|  | import java.util.logging.Logger; | 
|  | import java.util.regex.Matcher; | 
|  | import java.util.regex.Pattern; | 
|  | import javax.annotation.concurrent.Immutable; | 
|  |  | 
|  | /** | 
|  | * Represents a fully qualified name for an android resource. | 
|  | * | 
|  | * <p>Each resource name consists of the resource package, name, type, and qualifiers. | 
|  | */ | 
|  | // TODO(b/146498565): remove and/or replace.  For normal resources this can just be a ResourceName | 
|  | // with Configuration, and the latter can come from aapt2 directly.  tools:* attributes should be a | 
|  | // separate DataKey; as FullyQualifiedName they all have the same package, same singleton | 
|  | // VirtualType, and same empty qualifiers. | 
|  | @Immutable | 
|  | public class FullyQualifiedName implements DataKey { | 
|  | 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; | 
|  | // TODO(b/146498565): use com.android.aapt.ConfigurationOuterClass.Configuration. | 
|  | private final ImmutableList<String> qualifiers; | 
|  | private final Type type; | 
|  | private final String name; | 
|  |  | 
|  | private FullyQualifiedName(String pkg, ImmutableList<String> qualifiers, Type type, String name) { | 
|  | Preconditions.checkArgument(!pkg.isEmpty()); | 
|  | this.pkg = pkg; | 
|  | this.qualifiers = qualifiers; | 
|  | this.type = type; | 
|  | this.name = name; | 
|  | } | 
|  |  | 
|  | private static Type createTypeFrom(String rawType) { | 
|  | ResourceType resourceType = ResourceTypeEnum.get(rawType); | 
|  | VirtualType virtualType = VirtualType.getEnum(rawType); | 
|  | if (resourceType != null) { | 
|  | return new ResourceTypeWrapper(resourceType); | 
|  | } else if (virtualType != null) { | 
|  | return virtualType; | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates a new FullyQualifiedName with normalized qualifiers. | 
|  | * | 
|  | * @param rawPkg The resource package of the name. If unknown the default should be "res-auto" | 
|  | * @param qualifiers The resource qualifiers of the name, such as "en" or "xhdpi". | 
|  | * @param type The type of the name. | 
|  | * @param name The name of the name. | 
|  | * @return A new FullyQualifiedName. | 
|  | */ | 
|  | public static FullyQualifiedName of( | 
|  | String rawPkg, List<String> qualifiers, Type type, String name) { | 
|  | checkNotNull(rawPkg); | 
|  | checkNotNull(qualifiers); | 
|  | checkNotNull(type); | 
|  | checkNotNull(name); | 
|  | ImmutableList<String> immutableQualifiers = ImmutableList.copyOf(qualifiers); | 
|  | String pkg = rawPkg.isEmpty() ? DEFAULT_PACKAGE : rawPkg; | 
|  | // 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); | 
|  | // 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); | 
|  | if (cached == null) { | 
|  | return fqn; | 
|  | } else { | 
|  | cacheHit.incrementAndGet(); | 
|  | return cached; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates a new FullyQualifiedName with normalized qualifiers. | 
|  | * | 
|  | * @param pkg The resource package of the name. If unknown the default should be "res-auto" | 
|  | * @param qualifiers The resource qualifiers of the name, such as "en" or "xhdpi". | 
|  | * @param type The resource type of the name. | 
|  | * @param name The name of the name. | 
|  | * @return A new FullyQualifiedName. | 
|  | */ | 
|  | static FullyQualifiedName of( | 
|  | String pkg, List<String> qualifiers, ResourceType type, String name) { | 
|  | return of(pkg, qualifiers, new ResourceTypeWrapper(type), name); | 
|  | } | 
|  |  | 
|  | public static FullyQualifiedName fromProto(SerializeFormat.DataKey protoKey) { | 
|  | return of( | 
|  | protoKey.getKeyPackage(), | 
|  | protoKey.getQualifiersList(), | 
|  | createTypeFrom(protoKey.getResourceType()), | 
|  | protoKey.getKeyValue()); | 
|  | } | 
|  |  | 
|  | // Note that while "$" is not allowed in source code, it's used in generated names (precisely to | 
|  | // avoid colliding with source code). | 
|  | static final Pattern QUALIFIED_REFERENCE = | 
|  | Pattern.compile("^((?<package>[^:]+):)?(?<type>\\w+)/(?<name>[A-Za-z0-9_.$]+)$"); | 
|  |  | 
|  | public static FullyQualifiedName fromReference( | 
|  | String qualifiedReference, Optional<String> packageName) { | 
|  | final Matcher matcher = QUALIFIED_REFERENCE.matcher(qualifiedReference); | 
|  | Preconditions.checkArgument( | 
|  | matcher.matches(), | 
|  | "%s is not a reference. Expected %s", | 
|  | qualifiedReference, | 
|  | QUALIFIED_REFERENCE.pattern()); | 
|  | return of( | 
|  | Optional.ofNullable(emptyToNull(matcher.group("package"))) | 
|  | .orElse(packageName.orElse(DEFAULT_PACKAGE)), | 
|  | ImmutableList.of(), | 
|  | ResourceTypeEnum.get(matcher.group("type")), | 
|  | matcher.group("name")); | 
|  | } | 
|  |  | 
|  | public static void logCacheUsage(Logger logger) { | 
|  | logger.fine( | 
|  | String.format( | 
|  | "Total FullyQualifiedName instance cache hits %s out of %s", | 
|  | cacheHit.intValue(), instanceCache.size())); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a string path representation of the FullyQualifiedName. | 
|  | * | 
|  | * <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. | 
|  | */ | 
|  | public String toPathString(Path source) { | 
|  | String sourceExtension = FullyQualifiedName.Factory.getSourceExtension(source); | 
|  | return Paths.get( | 
|  | DASH_JOINER.join( | 
|  | ImmutableList.<String>builder().add(type.getName()).addAll(qualifiers).build()), | 
|  | name + sourceExtension) | 
|  | .toString(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toPrettyString() { | 
|  | // TODO(corysmith): Add package when we start tracking it. | 
|  | return String.format( | 
|  | "%s%s/%s", | 
|  | DEFAULT_PACKAGE.equals(pkg) ? "" : pkg + ':', | 
|  | DASH_JOINER.join( | 
|  | ImmutableList.<String>builder().add(type.getName()).addAll(qualifiers).build()), | 
|  | name); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the string path representation of the values directory and qualifiers. | 
|  | * | 
|  | * <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 | 
|  | // logically, the FullyQualifiedName should just be able to provide the relative path string for | 
|  | // the resource. | 
|  | public String valuesPath() { | 
|  | return Paths.get( | 
|  | DASH_JOINER.join( | 
|  | ImmutableList.<String>builder().add("values").addAll(qualifiers).build()), | 
|  | "values.xml") | 
|  | .toString(); | 
|  | } | 
|  |  | 
|  | @VisibleForTesting | 
|  | public String asUnqualifedName() { | 
|  | return String.format( | 
|  | "%s/%s", | 
|  | DASH_JOINER.join( | 
|  | ImmutableList.<String>builder().add(type.getName()).addAll(qualifiers).build()), | 
|  | name); | 
|  | } | 
|  |  | 
|  | public String name() { | 
|  | return name; | 
|  | } | 
|  |  | 
|  | public boolean isInPackage(String packageName) { | 
|  | return pkg.equals(packageName); | 
|  | } | 
|  |  | 
|  | /** Provides the name qualified by the package it belongs to. */ | 
|  | public String qualifiedName() { | 
|  | return (pkg.equals(DEFAULT_PACKAGE) ? "" : pkg + ":") + name; | 
|  | } | 
|  |  | 
|  | public String asQualifiedReference() { | 
|  | return String.format( | 
|  | "%s%s/%s", (pkg.equals(DEFAULT_PACKAGE) ? "" : pkg + ":"), type.getName(), name); | 
|  | } | 
|  |  | 
|  | public ResourceType type() { | 
|  | if (type instanceof ResourceTypeWrapper) { | 
|  | return ((ResourceTypeWrapper) type).resourceType; | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | public boolean isOverwritable() { | 
|  | return type.isOverwritable(this); | 
|  | } | 
|  |  | 
|  | /** Creates a FullyQualifiedName from this one with a different package. */ | 
|  | @CheckReturnValue | 
|  | public FullyQualifiedName replacePackage(String newPackage) { | 
|  | if (pkg.equals(newPackage)) { | 
|  | return this; | 
|  | } | 
|  | return of(newPackage, qualifiers, type, name); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int hashCode() { | 
|  | return Objects.hash(pkg, qualifiers, type, name); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object obj) { | 
|  | if (!(obj instanceof FullyQualifiedName)) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | FullyQualifiedName other = getClass().cast(obj); | 
|  | return Objects.equals(pkg, other.pkg) | 
|  | && Objects.equals(type, other.type) | 
|  | && Objects.equals(name, other.name) | 
|  | && Objects.equals(qualifiers, other.qualifiers); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return MoreObjects.toStringHelper(getClass()) | 
|  | .add("pkg", pkg) | 
|  | .add("qualifiers", qualifiers) | 
|  | .add("type", type) | 
|  | .add("name", name) | 
|  | .toString(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int compareTo(DataKey otherKey) { | 
|  | if (!(otherKey instanceof FullyQualifiedName)) { | 
|  | return getKeyType().compareTo(otherKey.getKeyType()); | 
|  | } | 
|  | FullyQualifiedName other = (FullyQualifiedName) otherKey; | 
|  | if (!pkg.equals(other.pkg)) { | 
|  | return pkg.compareTo(other.pkg); | 
|  | } | 
|  | if (!type.equals(other.type)) { | 
|  | return type.compareTo(other.type); | 
|  | } | 
|  | if (!name.equals(other.name)) { | 
|  | return name.compareTo(other.name); | 
|  | } | 
|  | if (!qualifiers.equals(other.qualifiers)) { | 
|  | if (qualifiers.size() != other.qualifiers.size()) { | 
|  | return qualifiers.size() - other.qualifiers.size(); | 
|  | } | 
|  | // This works because the qualifiers are always in an ordered sequence. | 
|  | return qualifiers.toString().compareTo(other.qualifiers.toString()); | 
|  | } | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public KeyType getKeyType() { | 
|  | return KeyType.FULL_QUALIFIED_NAME; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean shouldDetectConflicts() { | 
|  | // Ignore conflicts among pseudolocales. | 
|  | return qualifiers.stream() | 
|  | .noneMatch(q -> Ascii.equalsIgnoreCase(q, "en-rXA") || Ascii.equalsIgnoreCase(q, "ar-rXB")); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void serializeTo(OutputStream out, int valueSize) throws IOException { | 
|  | toSerializedBuilder().setValueSize(valueSize).build().writeDelimitedTo(out); | 
|  | } | 
|  |  | 
|  | public SerializeFormat.DataKey.Builder toSerializedBuilder() { | 
|  | return SerializeFormat.DataKey.newBuilder() | 
|  | .setKeyPackage(pkg) | 
|  | .setResourceType(type.getName()) | 
|  | .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 VirtualType(String name, String displayName) { | 
|  | this.name = name; | 
|  | } | 
|  |  | 
|  | /** 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; | 
|  | } | 
|  |  | 
|  | @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 { | 
|  | String getName(); | 
|  |  | 
|  | ConcreteType getType(); | 
|  |  | 
|  | boolean isOverwritable(FullyQualifiedName fqn); | 
|  |  | 
|  | int compareTo(Type other); | 
|  |  | 
|  | @Override | 
|  | boolean equals(Object obj); | 
|  |  | 
|  | @Override | 
|  | int hashCode(); | 
|  |  | 
|  | @Override | 
|  | String toString(); | 
|  |  | 
|  | /** | 
|  | * The category of type that a {@link Type} can be. | 
|  | * | 
|  | * <p><em>Note:</em> used for strict ordering of {@link FullyQualifiedName}s. | 
|  | */ | 
|  | 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(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** Represents the configuration qualifiers in a resource directory. */ | 
|  | public static class Qualifiers { | 
|  |  | 
|  | private static final Qualifiers EMPTY_QUALIFIERS = | 
|  | new Qualifiers(null, ImmutableList.of(), false); | 
|  |  | 
|  | // Qualifiers are reasonably expensive to create, so cache them on directory names. | 
|  | private static final ConcurrentMap<String, Qualifiers> qualifierCache = | 
|  | new ConcurrentHashMap<>(); | 
|  |  | 
|  | public static final String INVALID_QUALIFIERS = "%s contains invalid qualifiers."; | 
|  | private final ResourceFolderType folderType; | 
|  | private final ImmutableList<String> qualifiers; | 
|  | private boolean defaultLocale; | 
|  |  | 
|  | private Qualifiers( | 
|  | ResourceFolderType folderType, ImmutableList<String> qualifiers, boolean defaultLocale) { | 
|  | this.folderType = folderType; | 
|  | this.qualifiers = qualifiers; | 
|  | this.defaultLocale = defaultLocale; | 
|  | } | 
|  |  | 
|  | public static Qualifiers parseFrom(String directoryName) { | 
|  | return qualifierCache.computeIfAbsent( | 
|  | directoryName, d -> getQualifiers(Splitter.on(SdkConstants.RES_QUALIFIER_SEP).split(d))); | 
|  | } | 
|  |  | 
|  | private static Qualifiers getQualifiers(String... dirNameAndQualifiers) { | 
|  | return getQualifiers(Arrays.asList(dirNameAndQualifiers)); | 
|  | } | 
|  |  | 
|  | private static Qualifiers getQualifiers(Iterable<String> dirNameAndQualifiers) { | 
|  | PeekingIterator<String> rawQualifiers = | 
|  | Iterators.peekingIterator(dirNameAndQualifiers.iterator()); | 
|  | // Remove directory name | 
|  | final ResourceFolderType folderType = ResourceFolderType.getTypeByName(rawQualifiers.next()); | 
|  |  | 
|  | // If there is no folder type, there are no qualifiers to parse. | 
|  | if (folderType == null) { | 
|  | return EMPTY_QUALIFIERS; | 
|  | } | 
|  |  | 
|  | List<String> handledQualifiers = new ArrayList<>(); | 
|  | // Do some substitution of language/region qualifiers. | 
|  | while (rawQualifiers.hasNext()) { | 
|  | handledQualifiers.add(rawQualifiers.next()); | 
|  | } | 
|  | // 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.normalizeByAddingImpliedVersionQualifier(); | 
|  |  | 
|  | ImmutableList.Builder<String> builder = ImmutableList.<String>builder(); | 
|  | // index 3 is past the country code, network code, and locale indices. | 
|  | for (int i = 0; i < FolderConfiguration.getQualifierCount(); ++i) { | 
|  | addIfNotNull(config.getQualifier(i), builder); | 
|  | } | 
|  | ResourceQualifier localeQualifier = config.getLocaleQualifier(); | 
|  | return new Qualifiers( | 
|  | folderType, | 
|  | builder.build(), | 
|  | localeQualifier == null || localeQualifier == localeQualifier.getNullQualifier()); | 
|  | } | 
|  |  | 
|  | private static void addIfNotNull( | 
|  | ResourceQualifier qualifier, ImmutableList.Builder<String> builder) { | 
|  | if (qualifier != null && qualifier != qualifier.getNullQualifier()) { | 
|  | builder.add(qualifier.getFolderSegment()); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** Returns the qualifiers as a list of strings. */ | 
|  | public List<String> asList() { | 
|  | return qualifiers; | 
|  | } | 
|  |  | 
|  | public ResourceFolderType asFolderType() { | 
|  | return folderType; | 
|  | } | 
|  |  | 
|  | /** Creates a Qualifiers assuming that they are in the values directory. */ | 
|  | @VisibleForTesting | 
|  | public static Qualifiers forValuesFolderFrom(List<String> qualifiers) { | 
|  | return Qualifiers.getQualifiers( | 
|  | ImmutableList.builder().add("values").addAll(qualifiers).build().toArray(new String[0])); | 
|  | } | 
|  |  | 
|  | public boolean containDefaultLocale() { | 
|  | return defaultLocale; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** A factory for parsing a 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() | 
|  | .addAll(ResourceType.getClassNames()) | 
|  | .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() | 
|  | .addAll(ResourceType.getClassNames()) | 
|  | .add(VirtualType.getNames()) | 
|  | .build())); | 
|  | 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 final Qualifiers qs; | 
|  |  | 
|  | private Factory(Qualifiers qualifiers, String pkg) { | 
|  | // this.qualifiers = qualifiers; | 
|  | this.pkg = pkg; | 
|  | this.qs = qualifiers; | 
|  | } | 
|  |  | 
|  | /** Creates a factory with default package from a directory name split on '-'. */ | 
|  | @VisibleForTesting | 
|  | public static Factory fromDirectoryName(String... dirNameAndQualifiers) { | 
|  | return using(Qualifiers.getQualifiers(dirNameAndQualifiers), DEFAULT_PACKAGE); | 
|  | } | 
|  |  | 
|  | /** Creates a factory with default package from a directory with '-' separating qualifiers. */ | 
|  | public static Factory fromDirectoryName(String dirNameAndQualifiers) { | 
|  | return using(Qualifiers.parseFrom(dirNameAndQualifiers), DEFAULT_PACKAGE); | 
|  | } | 
|  |  | 
|  | @VisibleForTesting | 
|  | public static Factory from(List<String> qualifiers, String pkg) { | 
|  | return using(Qualifiers.forValuesFolderFrom(qualifiers), pkg); | 
|  | } | 
|  |  | 
|  | @VisibleForTesting | 
|  | public static Factory from(List<String> qualifiers) { | 
|  | return from(qualifiers, DEFAULT_PACKAGE); | 
|  | } | 
|  |  | 
|  | /** Creates a factory with the qualifiers and package. */ | 
|  | public static Factory using(Qualifiers qualifiers) { | 
|  | return using(qualifiers, DEFAULT_PACKAGE); | 
|  | } | 
|  |  | 
|  | /** Creates a factory with the qualifiers and package. */ | 
|  | public static Factory using(Qualifiers qualifiers, String pkg) { | 
|  | return new Factory(qualifiers, pkg.isEmpty() ? DEFAULT_PACKAGE : pkg); | 
|  | } | 
|  |  | 
|  | 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, qs.asList(), 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 = | 
|  | 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, qs.asList(), 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 String firstNonNull(String... values) { | 
|  | for (String value : values) { | 
|  | if (value != null) { | 
|  | return value; | 
|  | } | 
|  | } | 
|  | throw new NullPointerException("Expected a nonnull value."); | 
|  | } | 
|  | } | 
|  | } |