| // 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.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.xml.ResourcesAttribute; |
| 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.CheckReturnValue; |
| 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. |
| */ |
| @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; |
| 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 = ResourceType.getEnum(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()); |
| } |
| |
| static final Pattern QUALIFIED_REFERENCE = |
| Pattern.compile("((?<package>[^:]+):)?(?<type>\\w+)/(?<name>\\w+)"); |
| |
| public static FullyQualifiedName fromReference( |
| String qualifiedReference, Optional<String> packageName) { |
| final Matcher matcher = QUALIFIED_REFERENCE.matcher(qualifiedReference); |
| Preconditions.checkArgument( |
| matcher.find(), |
| "%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(), |
| ResourceType.getEnum(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 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 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 { |
| 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()) { |
| String qualifier = rawQualifiers.next(); |
| if ("es".equalsIgnoreCase(qualifier) |
| && rawQualifiers.hasNext() |
| && "419".equalsIgnoreCase(rawQualifiers.peek())) { |
| // Replace the es-419. |
| handledQualifiers.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. |
| handledQualifiers.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(); |
| |
| 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); |
| } |
| return new Qualifiers(folderType, builder.build(), config.getLocaleQualifier() == null); |
| } |
| |
| private static void addIfNotNull( |
| ResourceQualifier qualifier, ImmutableList.Builder<String> builder) { |
| if (qualifier != null) { |
| 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 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())); |
| 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(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, qs.asList(), 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)); |
| } |
| } |
| } |