// Copyright 2017 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.resources;
import com.android.builder.core.VariantConfiguration;
import com.android.builder.dependency.SymbolFileProvider;
import com.android.resources.ResourceType;
import com.android.utils.ILogger;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Table;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/** This provides a unified interface for working with R.txt symbols files. */
public class ResourceSymbols {
private static final Logger logger = Logger.getLogger(ResourceSymbols.class.getCanonicalName());
/** Represents a resource symbol with a value. */
// Forked from com.android.builder.internal.SymbolLoader.SymbolEntry.
static class RTxtSymbolEntry {
private final String name;
private final String type;
private final String value;
RTxtSymbolEntry(String name, String type, String value) {
this.name = name;
this.type = type;
this.value = value;
}
public String getValue() {
return value;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public FieldInitializer toInitializer() {
if (type.equals("int")) {
return IntFieldInitializer.of(name, value);
}
Preconditions.checkArgument(type.equals("int[]"));
return IntArrayFieldInitializer.of(name, value);
}
}
/** Task to load and parse R.txt symbols */
private static final class SymbolLoadingTask implements Callable<ResourceSymbols> {
private final Path rTxtSymbols;
SymbolLoadingTask(Path symbolFile) {
this.rTxtSymbols = symbolFile;
}
@Override
public ResourceSymbols call() throws Exception {
List<String> lines = Files.readAllLines(rTxtSymbols, StandardCharsets.UTF_8);
Table<String, String, RTxtSymbolEntry> symbols = HashBasedTable.create();
for (int lineIndex = 1; lineIndex <= lines.size(); lineIndex++) {
String line = null;
try {
line = lines.get(lineIndex - 1);
// format is "<type> <class> <name> <value>"
// don't want to split on space as value could contain spaces.
int pos = line.indexOf(' ');
String type = line.substring(0, pos);
int pos2 = line.indexOf(' ', pos + 1);
String className = line.substring(pos + 1, pos2);
int pos3 = line.indexOf(' ', pos2 + 1);
String name = line.substring(pos2 + 1, pos3);
String value = line.substring(pos3 + 1);
symbols.put(className, name, new RTxtSymbolEntry(name, type, value));
} catch (IndexOutOfBoundsException e) {
String s =
String.format(
"File format error reading %s\tline %d: '%s'",
rTxtSymbols.toString(), lineIndex, line);
logger.severe(s);
throw new IOException(s, e);
}
}
return ResourceSymbols.from(symbols);
}
}
private static final class PackageParsingTask implements Callable<String> {
private final File manifest;
PackageParsingTask(File manifest) {
this.manifest = manifest;
}
@Override
public String call() throws Exception {
return VariantConfiguration.getManifestPackage(manifest);
}
}
/**
* Loads the SymbolTables from a list of SymbolFileProviders.
*
* @param dependencies The full set of library symbols to load.
* @param executor The executor use during loading.
* @param iLogger Android logger to use.
* @param packageToExclude A string package to elide if it exists in the providers.
* @return A list of loading {@link ResourceSymbols} instances.
* @throws ExecutionException
* @throws InterruptedException when there is an error loading the symbols.
*/
public static Multimap<String, ListenableFuture<ResourceSymbols>> loadFrom(
Collection<SymbolFileProvider> dependencies,
ListeningExecutorService executor,
ILogger iLogger,
@Nullable String packageToExclude)
throws InterruptedException, ExecutionException {
Map<SymbolFileProvider, ListenableFuture<String>> providerToPackage = new HashMap<>();
for (SymbolFileProvider dependency : dependencies) {
providerToPackage.put(
dependency, executor.submit(new PackageParsingTask(dependency.getManifest())));
}
Multimap<String, ListenableFuture<ResourceSymbols>> packageToTable = HashMultimap.create();
for (Entry<SymbolFileProvider, ListenableFuture<String>> entry : providerToPackage.entrySet()) {
File symbolFile = entry.getKey().getSymbolFile();
if (!Objects.equals(entry.getValue().get(), packageToExclude)) {
packageToTable.put(entry.getValue().get(), load(symbolFile.toPath(), executor));
}
}
return packageToTable;
}
public static ResourceSymbols from(Table<String, String, RTxtSymbolEntry> table) {
return new ResourceSymbols(table);
}
public static ResourceSymbols merge(Collection<ResourceSymbols> symbolTables) {
final Table<String, String, RTxtSymbolEntry> mergedTable = HashBasedTable.create();
for (ResourceSymbols symbolTableProvider : symbolTables) {
mergedTable.putAll(symbolTableProvider.asTable());
}
return from(mergedTable);
}
/** Read the symbols from the provided symbol file. */
public static ListenableFuture<ResourceSymbols> load(
Path primaryRTxt, ListeningExecutorService executorService) {
return executorService.submit(new SymbolLoadingTask(primaryRTxt));
}
private final Table<String, String, RTxtSymbolEntry> values;
private ResourceSymbols(Table<String, String, RTxtSymbolEntry> values) {
this.values = values;
}
public Table<String, String, RTxtSymbolEntry> asTable() {
return values;
}
/**
* Writes the java sources for a given package.
*
* @param sourceOut The directory to write the java package structures and sources to.
* @param packageName The name of the package to write.
* @param packageSymbols The symbols defined in the given package.
* @param finalFields
* @throws IOException when encountering an error during writing.
*/
public void writeTo(
Path sourceOut,
String packageName,
Collection<ResourceSymbols> packageSymbols,
boolean finalFields)
throws IOException {
Map<ResourceType, Set<String>> symbols = new EnumMap<>(ResourceType.class);
for (ResourceSymbols packageSymbol : packageSymbols) {
symbols.putAll(packageSymbol.asFilterMap());
}
RSourceGenerator.with(sourceOut, asInitializers(), finalFields).write(packageName, symbols);
}
public FieldInitializers asInitializers() {
SetMultimap<ResourceType, FieldInitializer> valuesOut =
MultimapBuilder.enumKeys(ResourceType.class).treeSetValues().build();
Table<String, String, RTxtSymbolEntry> symbolTable = asTable();
for (String typeName : symbolTable.rowKeySet()) {
final ResourceType type = ResourceType.getEnum(typeName);
for (RTxtSymbolEntry symbolEntry : symbolTable.row(typeName).values()) {
valuesOut.put(type, symbolEntry.toInitializer());
}
}
return FieldInitializers.copyOf(valuesOut.asMap());
}
public Map<ResourceType, Set<String>> asFilterMap() {
Map<ResourceType, Set<String>> filter = new EnumMap<>(ResourceType.class);
Table<String, String, RTxtSymbolEntry> symbolTable = asTable();
for (String typeName : symbolTable.rowKeySet()) {
Set<String> fields = new HashSet<>();
for (RTxtSymbolEntry symbolEntry : symbolTable.row(typeName).values()) {
fields.add(symbolEntry.getName());
}
if (!fields.isEmpty()) {
filter.put(ResourceType.getEnum(typeName), fields);
}
}
return filter;
}
}