// Copyright 2015 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.android.builder.core.VariantType; import com.android.manifmerger.ManifestMerger2; import com.android.manifmerger.ManifestMerger2.MergeType; import com.android.repository.Revision; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.aapt2.CompiledResources; import com.google.devtools.build.android.aapt2.StaticLibrary; import com.google.devtools.common.options.Converter; import com.google.devtools.common.options.EnumConverter; import com.google.devtools.common.options.OptionsParsingException; import java.io.File; import java.lang.reflect.ParameterizedType; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Some convenient converters used by android actions. Note: These are specific to android actions. */ public final class Converters { private static final Converter IDENTITY_CONVERTER = new Converter() { @Override public String convert(String input) { return input; } @Override public String getTypeDescription() { return "a string"; } }; /** * Converter for {@link UnvalidatedAndroidData}. Relies on {@code * UnvalidatedAndroidData#valueOf(String)} to perform conversion and validation. */ public static class UnvalidatedAndroidDataConverter implements Converter { @Override public UnvalidatedAndroidData convert(String input) throws OptionsParsingException { try { return UnvalidatedAndroidData.valueOf(input); } catch (IllegalArgumentException e) { throw new OptionsParsingException( String.format("invalid UnvalidatedAndroidData: %s", e.getMessage()), e); } } @Override public String getTypeDescription() { return "unvalidated android data in the format " + UnvalidatedAndroidData.EXPECTED_FORMAT; } } /** Converter for {@link UnvalidatedAndroidDirectories}. */ public static class UnvalidatedAndroidDirectoriesConverter implements Converter { @Override public UnvalidatedAndroidDirectories convert(String input) throws OptionsParsingException { try { return UnvalidatedAndroidDirectories.valueOf(input); } catch (IllegalArgumentException e) { throw new OptionsParsingException( String.format("invalid UnvalidatedAndroidDirectories: %s", e.getMessage()), e); } } @Override public String getTypeDescription() { return "unvalidated android directories in the format " + UnvalidatedAndroidDirectories.EXPECTED_FORMAT; } } /** * Converter for a list of {@link DependencyAndroidData}. Relies on {@code * DependencyAndroidData#valueOf(String)} to perform conversion and validation. */ public static class DependencyAndroidDataListConverter implements Converter> { @Override public List convert(String input) throws OptionsParsingException { if (input.isEmpty()) { return ImmutableList.of(); } try { ImmutableList.Builder builder = ImmutableList.builder(); for (String item : input.split(",")) { builder.add(DependencyAndroidData.valueOf(item)); } return builder.build(); } catch (IllegalArgumentException e) { throw new OptionsParsingException( String.format("invalid DependencyAndroidData: %s", e.getMessage()), e); } } @Override public String getTypeDescription() { return "a list of dependency android data in the format " + DependencyAndroidData.EXPECTED_FORMAT + "[,...]"; } } /** Converter for a {@link SerializedAndroidData}. */ public static class SerializedAndroidDataConverter implements Converter { @Override public SerializedAndroidData convert(String input) throws OptionsParsingException { try { return SerializedAndroidData.valueOf(input); } catch (IllegalArgumentException e) { throw new OptionsParsingException( String.format("invalid SerializedAndroidData: %s", e.getMessage()), e); } } @Override public String getTypeDescription() { return "preparsed android data in the format " + SerializedAndroidData.EXPECTED_FORMAT; } } /** Converter for a list of {@link SerializedAndroidData}. */ public static class SerializedAndroidDataListConverter implements Converter> { @Override public List convert(String input) throws OptionsParsingException { if (input.isEmpty()) { return ImmutableList.of(); } try { ImmutableList.Builder builder = ImmutableList.builder(); for (String entry : input.split("&")) { builder.add(SerializedAndroidData.valueOf(entry)); } return builder.build(); } catch (IllegalArgumentException e) { throw new OptionsParsingException( String.format("invalid SerializedAndroidData: %s", e.getMessage()), e); } } @Override public String getTypeDescription() { return "a list of preparsed android data in the format " + SerializedAndroidData.EXPECTED_FORMAT + "[&...]"; } } /** Converter for a single {@link DependencySymbolFileProvider}. */ public static class DependencySymbolFileProviderConverter implements Converter { @Override public DependencySymbolFileProvider convert(String input) throws OptionsParsingException { try { return DependencySymbolFileProvider.valueOf(input); } catch (IllegalArgumentException e) { throw new OptionsParsingException( String.format("invalid DependencyAndroidData: %s", e.getMessage()), e); } } @Override public String getTypeDescription() { return String.format( "a dependency android data in the format: %s[%s]", DependencySymbolFileProvider.commandlineFormat("1"), DependencySymbolFileProvider.commandlineFormat("2")); } } /** * Converter for {@link Revision}. Relies on {@code Revision#parseRevision(String)} to perform * conversion and validation. */ public static class RevisionConverter implements Converter { @Override public Revision convert(String input) throws OptionsParsingException { try { return Revision.parseRevision(input); } catch (NumberFormatException e) { throw new OptionsParsingException(e.getMessage()); } } @Override public String getTypeDescription() { return "a revision number"; } } /** Validating converter for Paths. A Path is considered valid if it resolves to a file. */ public static class PathConverter implements Converter { private final boolean mustExist; public PathConverter() { this.mustExist = false; } protected PathConverter(boolean mustExist) { this.mustExist = mustExist; } @Override public Path convert(String input) throws OptionsParsingException { try { Path path = FileSystems.getDefault().getPath(input); if (mustExist && !Files.exists(path)) { throw new OptionsParsingException( String.format("%s is not a valid path: it does not exist.", input)); } return path; } catch (InvalidPathException e) { throw new OptionsParsingException( String.format("%s is not a valid path: %s.", input, e.getMessage()), e); } } @Override public String getTypeDescription() { return "a valid filesystem path"; } } /** * Validating converter for Paths. A Path is considered valid if it resolves to a file and exists. */ public static class ExistingPathConverter extends PathConverter { public ExistingPathConverter() { super(true); } } /** Converter for {@link VariantType}. */ public static class VariantTypeConverter extends EnumConverter { public VariantTypeConverter() { super(VariantType.class, "variant type"); } } /** Converter for {@link ManifestMerger2}.{@link MergeType}. */ public static class MergeTypeConverter extends EnumConverter { public MergeTypeConverter() { super(MergeType.class, "merge type"); } } /** * Validating converter for a list of Paths. A Path is considered valid if it resolves to a file. */ @Deprecated public static class PathListConverter implements Converter> { private final PathConverter baseConverter; public PathListConverter() { this(false); } protected PathListConverter(boolean mustExist) { baseConverter = new PathConverter(mustExist); } @Override public List convert(String input) throws OptionsParsingException { List list = new ArrayList<>(); for (String piece : input.split(":")) { if (!piece.isEmpty()) { list.add(baseConverter.convert(piece)); } } return Collections.unmodifiableList(list); } @Override public String getTypeDescription() { return "a colon-separated list of paths"; } } // Commas that are not escaped by a backslash. private static final String UNESCAPED_COMMA_REGEX = "(? implements Converter> { private final Converter keyConverter; private final Converter valueConverter; public DictionaryConverter(Converter keyConverter, Converter valueConverter) { this.keyConverter = keyConverter; this.valueConverter = valueConverter; } @Override public Map convert(String input) throws OptionsParsingException { if (input.isEmpty()) { return ImmutableMap.of(); } Map map = new LinkedHashMap<>(); // Only split on comma and colon that are not escaped with a backslash for (String entry : input.split(UNESCAPED_COMMA_REGEX)) { String[] entryFields = entry.split(UNESCAPED_COLON_REGEX, -1); if (entryFields.length < 2) { throw new OptionsParsingException( String.format( "Dictionary entry [%s] does not contain both a key and a value.", entry)); } else if (entryFields.length > 2) { throw new OptionsParsingException( String.format("Dictionary entry [%s] contains too many fields.", entry)); } // Unescape any comma or colon that is not a key or value separator. String keyString = unescapeInput(entryFields[0]); K key = keyConverter.convert(keyString); if (map.containsKey(key)) { throw new OptionsParsingException( String.format("Dictionary already contains the key [%s].", keyString)); } // Unescape any comma or colon that is not a key or value separator. String valueString = unescapeInput(entryFields[1]); V value = valueConverter.convert(valueString); map.put(key, value); } return ImmutableMap.copyOf(map); } @Override public String getTypeDescription() { // Retrieve types of dictionary through reflection to avoid overriding this method in each // subclass or passing types to this superclass. return String.format( "a comma-separated list of colon-separated key value pairs of the types %s and %s", ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0], ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[1]); } } /** * A converter for dictionary arguments of the format key:value[,key:value]*. The keys and values * may contain colons and commas as long as they are escaped with a backslash. The key and value * types are both String. */ public static class StringDictionaryConverter extends DictionaryConverter { public StringDictionaryConverter() { super(IDENTITY_CONVERTER, IDENTITY_CONVERTER); } // The way {@link OptionsData} checks for generic types requires convert to have literal type // parameters and not argument type parameters. @Override public Map convert(String input) throws OptionsParsingException { return super.convert(input); } } /** * A converter for dictionary arguments of the format key:value[,key:value]*. The keys and values * may contain colons and commas as long as they are escaped with a backslash. The key type is * Path and the value type is String. */ public static class ExistingPathStringDictionaryConverter extends DictionaryConverter { public ExistingPathStringDictionaryConverter() { super(new ExistingPathConverter(), IDENTITY_CONVERTER); } // The way {@link OptionsData} checks for generic types requires convert to have literal type // parameters and not argument type parameters. @Override public Map convert(String input) throws OptionsParsingException { return super.convert(input); } } /** Converts a list of static library strings into paths. */ @Deprecated public static class StaticLibraryListConverter implements Converter> { static final Splitter SPLITTER = Splitter.on(File.pathSeparatorChar); static final StaticLibraryConverter libraryConverter = new StaticLibraryConverter(); @Override public List convert(String input) throws OptionsParsingException { final ImmutableList.Builder builder = ImmutableList.builder(); for (String path : SPLITTER.splitToList(input)) { builder.add(libraryConverter.convert(path)); } return builder.build(); } @Override public String getTypeDescription() { return "Static resource libraries."; } } /** Converts a static library string into path. */ public static class StaticLibraryConverter implements Converter { static final PathConverter pathConverter = new PathConverter(true); @Override public StaticLibrary convert(String input) throws OptionsParsingException { return StaticLibrary.from(pathConverter.convert(input)); } @Override public String getTypeDescription() { return "Static resource library."; } } /** Converts a string of resources and manifest into paths. */ public static class CompiledResourcesConverter implements Converter { static final PathConverter pathConverter = new PathConverter(true); static final Pattern COMPILED_RESOURCE_FORMAT = Pattern.compile("(.+):(.+)"); @Override public CompiledResources convert(String input) throws OptionsParsingException { final Matcher matched = COMPILED_RESOURCE_FORMAT.matcher(input); if (!matched.find()) { throw new OptionsParsingException("Expected format :"); } Path resources = pathConverter.convert(matched.group(1)); Path manifest = pathConverter.convert(matched.group(2)); return CompiledResources.from(resources, manifest); } @Override public String getTypeDescription() { return "Compiled resources zip."; } } }