// 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 com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Ordering; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A parsed set of configuration filters for a split flag or an output filename. * * <p>The natural ordering of this class sorts by number of configurations, then by highest required * API version, if any, then by other specifiers (case-insensitive), with ties broken by the * filename or split flag originally used to create the instance (case-sensitive). * * <p>This has the following useful property:<br/> * Given two sets of {@link SplitConfigurationFilter}s, one from the input split flags, and * one from aapt's outputs... Each member of the output set can be matched to the greatest member * of the input set for which {@code input.matchesFilterFromFilename(output)} is true. */ final class SplitConfigurationFilter implements Comparable<SplitConfigurationFilter> { /** * Finds a mapping from filename suffixes to the split flags which could have spawned them. * * @param filenames The suffixes of the original apk filenames output by aapt, not including the * underscore used to set it off from the base filename or the base filename itself. * @param splitFlags The split flags originally passed to aapt. * @return A map whose keys are the filenames from {@code filenames} and whose values are * predictable filenames based on the split flags - that is, the commas present in the input * have been replaced with underscores. * @throws UnrecognizedSplitException if any of the inputs are unused or could not be matched */ static Map<String, String> mapFilenamesToSplitFlags( Iterable<String> filenames, Iterable<String> splitFlags) throws UnrecognizedSplitsException { TreeSet<SplitConfigurationFilter> filenameFilters = new TreeSet<>(); for (String filename : filenames) { filenameFilters.add(SplitConfigurationFilter.fromFilenameSuffix(filename)); } TreeSet<SplitConfigurationFilter> flagFilters = new TreeSet<>(); for (String splitFlag : splitFlags) { flagFilters.add(SplitConfigurationFilter.fromSplitFlag(splitFlag)); } ImmutableMap.Builder<String, String> result = ImmutableMap.builder(); List<String> unidentifiedFilenames = new ArrayList<>(); for (SplitConfigurationFilter filenameFilter : filenameFilters) { Optional<SplitConfigurationFilter> matched = Iterables.tryFind(flagFilters, new MatchesFilterFromFilename(filenameFilter)); if (matched.isPresent()) { result.put(filenameFilter.filename, matched.get().filename); flagFilters.remove(matched.get()); } else { unidentifiedFilenames.add(filenameFilter.filename); } } if (!(unidentifiedFilenames.isEmpty() && flagFilters.isEmpty())) { ImmutableList.Builder<String> unidentifiedFlags = ImmutableList.builder(); for (SplitConfigurationFilter flagFilter : flagFilters) { unidentifiedFlags.add(flagFilter.filename); } throw new UnrecognizedSplitsException( unidentifiedFlags.build(), unidentifiedFilenames, result.build()); } return result.build(); } /** * Exception thrown when mapFilenamesToSplitFlags fails to find matches for all elements of both * input sets. */ static final class UnrecognizedSplitsException extends Exception { private final ImmutableList<String> unidentifiedSplits; private final ImmutableList<String> unidentifiedFilenames; private final ImmutableMap<String, String> identifiedSplits; UnrecognizedSplitsException( Iterable<String> unidentifiedSplits, Iterable<String> unidentifiedFilenames, Map<String, String> identifiedSplits) { super( "Could not find matching filenames for these split flags:\n" + Joiner.on("\n").join(unidentifiedSplits) + "\nnor matching split flags for these filenames:\n" + Joiner.on(", ").join(unidentifiedFilenames) + "\nFound these (filename => split flag) matches though:\n" + Joiner.on("\n").withKeyValueSeparator(" => ").join(identifiedSplits)); this.unidentifiedSplits = ImmutableList.copyOf(unidentifiedSplits); this.unidentifiedFilenames = ImmutableList.copyOf(unidentifiedFilenames); this.identifiedSplits = ImmutableMap.copyOf(identifiedSplits); } /** Returns the list of split flags which did not find a match. */ ImmutableList<String> getUnidentifiedSplits() { return unidentifiedSplits; } /** Returns the list of filename suffixes which did not find a match. */ ImmutableList<String> getUnidentifiedFilenames() { return unidentifiedFilenames; } /** Returns the mapping from filename suffix to split flag for splits that did match. */ ImmutableMap<String, String> getIdentifiedSplits() { return identifiedSplits; } } /** Generates a SplitConfigurationFilter from a split flag. */ static SplitConfigurationFilter fromSplitFlag(String flag) { return SplitConfigurationFilter.fromFilenameSuffix(flag.replace(',', '_')); } /** Generates a SplitConfigurationFilter from the suffix of a split generated by aapt. */ static SplitConfigurationFilter fromFilenameSuffix(String suffix) { ImmutableSortedSet.Builder<ResourceConfiguration> configs = ImmutableSortedSet.reverseOrder(); for (String configuration : Splitter.on('_').split(suffix)) { configs.add(ResourceConfiguration.fromString(configuration)); } return new SplitConfigurationFilter(suffix, configs.build()); } /** * The suffix to be appended to the output package for this split configuration. * * <p>When created with {@link fromFilenameSuffix}, this will be the original filename from aapt; * when created with {@link fromSplitFlag}, this will be the filename to rename to. */ private final String filename; /** * A set of resource configurations which will be included in this split, sorted so that the * configs with the highest API versions come first. * * <p>It's okay for this to collapse duplicates, because aapt forbids duplicate resource * configurations across all splits in the same invocation anyway. */ private final ImmutableSortedSet<ResourceConfiguration> configs; private SplitConfigurationFilter( String filename, ImmutableSortedSet<ResourceConfiguration> configs) { this.filename = filename; this.configs = configs; } /** * Checks if the {@code other} split configuration filter could have been produced as a filename * by aapt based on this configuration filter being passed as a split flag. * * <p>This means that there must be a one-to-one mapping from each configuration in this filter to * a configuration in the {@code other} filter such that the non-API-version specifiers of the two * configurations match and the API version specifier of the {@code other} filter's configuration * is greater than or equal to the API version specifier of this filter's configuration. * * <p>Order of whole configurations doesn't matter, as aapt will reorder the configurations * according to complicated internal logic (yes, logic even more complicated than this!). * * <p>Care is needed with API version specifiers because aapt may add or change minimum * API version specifiers to configurations according to whether they had specifiers which are * only supported in certain versions of Android. It will only ever increase the minimum version * or leave it the same. * * <p>The other (non-wildcard) specifiers should be case-insensitive identical, including order; * aapt will not allow parts of a single configuration to be parsed out of order. * * @see ResourceConfiguration#matchesConfigurationFromFilename(ResourceConfiguration) */ boolean matchesFilterFromFilename(SplitConfigurationFilter filenameFilter) { if (filenameFilter.configs.size() != this.configs.size()) { return false; } List<ResourceConfiguration> unmatchedConfigs = new ArrayList<>(this.configs); for (ResourceConfiguration filenameConfig : filenameFilter.configs) { Optional<ResourceConfiguration> matched = Iterables.tryFind( unmatchedConfigs, new ResourceConfiguration.MatchesConfigurationFromFilename(filenameConfig)); if (!matched.isPresent()) { return false; } unmatchedConfigs.remove(matched.get()); } return true; } static final class MatchesFilterFromFilename implements Predicate<SplitConfigurationFilter> { private final SplitConfigurationFilter filenameFilter; MatchesFilterFromFilename(SplitConfigurationFilter filenameFilter) { this.filenameFilter = filenameFilter; } @Override public boolean apply(SplitConfigurationFilter flagFilter) { return flagFilter.matchesFilterFromFilename(filenameFilter); } } private static final Ordering<Iterable<ResourceConfiguration>> CONFIG_LEXICOGRAPHICAL = Ordering.natural().lexicographical(); @Override public int compareTo(SplitConfigurationFilter other) { return ComparisonChain.start() .compare(this.configs.size(), other.configs.size()) .compare(this.configs, other.configs, CONFIG_LEXICOGRAPHICAL) .compare(this.filename, other.filename) .result(); } @Override public int hashCode() { return Objects.hash(configs, filename); } @Override public boolean equals(Object object) { if (object instanceof SplitConfigurationFilter) { SplitConfigurationFilter other = (SplitConfigurationFilter) object; // the configs are derived from the filename, so we can be assured they are equal if the // filenames are. return Objects.equals(this.filename, other.filename); } return false; } @Override public String toString() { return "SplitConfigurationFilter{" + filename + "}"; } /** * An individual set of configuration specifiers, for the purposes of split name parsing. * * <p>The natural ordering of this class sorts by required API version, if any, then by other * specifiers. * * <p>This has the following useful property:<br/> * Given two sets of {@link ResourceConfiguration}s, one from an input split flag, and * one from aapt's output... Each member of the output set can be matched to the greatest member * of the input set for which {@code input.matchesConfigurationFromFilename(output)} is true. */ static final class ResourceConfiguration implements Comparable<ResourceConfiguration> { /** * Pattern to match wildcard parts ("any"), which can be safely ignored - aapt drops them. * * <p>Matches an 'any' part and the dash following it, or for an 'any' part which is the last * specifier, the dash preceding it. In the former case, it must be a full part - that is, * preceded by the beginning of the string or a dash, which will not be consumed. */ private static final Pattern WILDCARD_SPECIFIER = Pattern.compile("(?<=^|-)any(?:-|$)|-any$"); /** * Pattern to match the API version and capture the version number. * * <p>It must always be the last specifier in a config, although it may also be the first if * there are no other specifiers. */ private static final Pattern API_VERSION = Pattern.compile("(?:-|^)v(\\d+)$"); /** Parses a resource configuration into a form that can be compared to other configurations. */ static ResourceConfiguration fromString(String text) { // Case is ignored for resource configurations (aapt lowercases internally), // and wildcards can be dropped. String cleanSpecifiers = WILDCARD_SPECIFIER.matcher(text.toLowerCase(Locale.ENGLISH)).replaceAll(""); Matcher apiVersionMatcher = API_VERSION.matcher(cleanSpecifiers); if (apiVersionMatcher.find()) { return new ResourceConfiguration( cleanSpecifiers.substring(0, apiVersionMatcher.start()), Integer.parseInt(apiVersionMatcher.group(1))); } else { return new ResourceConfiguration(cleanSpecifiers, 0); } } /** The specifiers for this resource configuration, besides API version, in lowercase. */ private final String specifiers; /** The API version, or 0 to indicate that no API version was present in the original config. */ private final int apiVersion; private ResourceConfiguration(String specifiers, int apiVersion) { this.specifiers = specifiers; this.apiVersion = apiVersion; } /** * Checks that the {@code other} configuration could be a filename generated from this one. * * @see SplitConfigurationFilter#matchesFilterFromFilename(SplitConfigurationFilter) */ boolean matchesConfigurationFromFilename(ResourceConfiguration other) { return Objects.equals(other.specifiers, this.specifiers) && other.apiVersion >= this.apiVersion; } static final class MatchesConfigurationFromFilename implements Predicate<ResourceConfiguration> { private final ResourceConfiguration filenameConfig; MatchesConfigurationFromFilename(ResourceConfiguration filenameConfig) { this.filenameConfig = filenameConfig; } @Override public boolean apply(ResourceConfiguration flagConfig) { return flagConfig.matchesConfigurationFromFilename(filenameConfig); } } @Override public int compareTo(ResourceConfiguration other) { return ComparisonChain.start() .compare(this.apiVersion, other.apiVersion) .compare(this.specifiers, other.specifiers) .result(); } @Override public int hashCode() { return Objects.hash(specifiers, apiVersion); } @Override public boolean equals(Object object) { if (object instanceof ResourceConfiguration) { ResourceConfiguration other = (ResourceConfiguration) object; return Objects.equals(this.specifiers, other.specifiers) && this.apiVersion == other.apiVersion; } return false; } @Override public String toString() { return "ResourceConfiguration{" + specifiers + "-v" + Integer.toString(apiVersion) + "}"; } } }