/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.android;
import static com.google.common.base.Preconditions.checkNotNull;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.util.ObjectMappers;
import com.facebook.buck.util.XmlDomParser;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* This class implements a Buck build step that will generate a JSON file with the following info
* for each <string>, <plurals> and <string-array> resource found in the
* strings.xml files for each resource directory:
*
* <p>
*
* <pre>
* android_resource_name : {
* androidResourceId,
* stringsXmlPath
* }
* </pre>
*
* <p>where:
*
* <ul>
* <li>androidResourceId is the integer value, assigned by aapt, extracted from R.txt
* <li>stringsXmlPath is the path to the first strings.xml file where this string resource was
* found.
* </ul>
*
* <p>Example:
*
* <pre>
* {
* "strings": {
* "thread_list_new_message_button": {
* "androidResourceId":"0x7f0800e6",
* "stringsXmlPath":"android_res/com/facebook/messaging/res/values/strings.xml"
* },
* "bug_report_category_lock_screen": {
* "androidResourceId":"0x7f080489",
* "stringsXmlPath":"android_res/com/facebook/bugreporter/res/values/strings.xml"
* }
* },
* "plurals": {
* "like_count": {
* "androidResourceId": "0x7f080480",
* "stringsXmlPath":"android_res/com/facebook/likes/res/values/strings.xml"
* }
* },
* "string-arrays": {
* "share_types": {
* "androidResourceId": "0x7f080470",
* "stringsXmlPath":"android_res/com/facebook/share/res/values/strings.xml"
* }
* }
* }
* </pre>
*/
public class GenStringSourceMapStep extends AbstractExecutionStep {
private final ProjectFilesystem filesystem;
private final Path rDotTxtDir;
private final ImmutableList<Path> resDirectories;
private final Path destinationPath;
private final Map<String, Integer> stringResourceNameToIdMap = new HashMap<>();
private final Map<String, Integer> pluralsResourceNameToIdMap = new HashMap<>();
private final Map<String, Integer> arrayResourceNameToIdMap = new HashMap<>();
/**
* Associates each string resource with it's integer id (as assigned by {@code aapt} during
* GenRDotTxtUtil) and it's originating strings.xml file (path).
*
* @param rDotTxtDir Directory where {@code R.txt} is found
* @param resDirectories Directories of resource files. This the same list, with same ordering
* that was provided to {@code aapt} during GenRDotTxtUtil.
* @param destinationPath Directory where where {@code strings.json} is written to.
*/
public GenStringSourceMapStep(
ProjectFilesystem filesystem,
Path rDotTxtDir,
ImmutableList<Path> resDirectories,
Path destinationPath) {
super("build_string_source_map");
this.filesystem = filesystem;
this.rDotTxtDir = rDotTxtDir;
this.resDirectories = resDirectories;
this.destinationPath = destinationPath;
}
@Override
public StepExecutionResult execute(ExecutionContext context) {
// Read the R.txt file that was generated by aapt during GenRDotTxtUtil.
// This file contains all the resource names and the integer id that aapt assigned.
Path rDotTxtPath = rDotTxtDir.resolve("R.txt");
try {
CompileStringsStep.buildResourceNameToIdMap(
filesystem,
rDotTxtPath,
stringResourceNameToIdMap,
pluralsResourceNameToIdMap,
arrayResourceNameToIdMap);
} catch (FileNotFoundException ex) {
context.logError(ex, "The '%s' file is not present.", rDotTxtPath);
return StepExecutionResult.ERROR;
} catch (IOException ex) {
context.logError(ex, "Failure parsing R.txt file.");
return StepExecutionResult.ERROR;
}
Map<String, Map<String, NativeResourceInfo>> nativeStrings = parseStringFiles(context);
// write nativeStrings out to a file
Path outputPath = destinationPath.resolve("strings.json");
try {
ObjectMappers.WRITER.writeValue(
filesystem.getPathForRelativePath(outputPath).toFile(), nativeStrings);
} catch (IOException ex) {
context.logError(
ex, "Failed when trying to save the output file: '%s'", outputPath.toString());
return StepExecutionResult.ERROR;
}
return StepExecutionResult.SUCCESS;
}
private Map<String, Map<String, NativeResourceInfo>> parseStringFiles(ExecutionContext context) {
Map<String, NativeResourceInfo> nativeStrings = new HashMap<>();
Map<String, NativeResourceInfo> nativePlurals = new HashMap<>();
Map<String, NativeResourceInfo> nativeArrays = new HashMap<>();
for (Path resDir : resDirectories) {
Path stringsPath = resDir.resolve("values").resolve("strings.xml");
String stringsPathName = stringsPath.toString();
Path stringsFile = filesystem.getPathForRelativePath(stringsPath);
if (Files.exists(stringsFile)) {
try {
Document dom = XmlDomParser.parse(stringsFile);
NodeList stringNodes = dom.getElementsByTagName("string");
scrapeNodes(stringNodes, stringsPathName, nativeStrings, stringResourceNameToIdMap);
NodeList pluralNodes = dom.getElementsByTagName("plurals");
scrapeNodes(pluralNodes, stringsPathName, nativePlurals, pluralsResourceNameToIdMap);
NodeList arrayNodes = dom.getElementsByTagName("string-array");
scrapeNodes(arrayNodes, stringsPathName, nativeArrays, arrayResourceNameToIdMap);
} catch (IOException | SAXException ex) {
context.logError(ex, "Failed to parse strings file: '%s'", stringsPath);
}
}
}
Map<String, Map<String, NativeResourceInfo>> resultMap = new HashMap<>();
resultMap.put("strings", nativeStrings);
resultMap.put("plurals", nativePlurals);
resultMap.put("string-arrays", nativeArrays);
return resultMap;
}
/**
* Scrapes string resource names and values from the list of xml nodes passed and populates {@code
* stringsMap}, ignoring resource names that are already present in the map.
*
* @param nodes A list of XML nodes.
* @param nativeStrings Collection of native strings, only new ones will be added to it.
*/
@VisibleForTesting
static void scrapeNodes(
NodeList nodes,
String stringsFilePath,
Map<String, NativeResourceInfo> nativeStrings,
Map<String, Integer> resourceNameToIdMap) {
for (int i = 0; i < nodes.getLength(); ++i) {
Node node = nodes.item(i);
String resourceName = node.getAttributes().getNamedItem("name").getNodeValue();
if (!resourceNameToIdMap.containsKey(resourceName)) {
continue;
}
int resourceId = checkNotNull(resourceNameToIdMap.get(resourceName)).intValue();
// Add only new resources (don't overwrite existing ones)
if (!nativeStrings.containsKey(resourceName)) {
nativeStrings.put(resourceName, new NativeResourceInfo(resourceId, stringsFilePath));
}
}
}
/**
* This class manages the attributes for a <string> resource that we obtain from parsing the
* various strings.xml files. As information is cross-referenced with other sources, the combined
* set of knowledge for each string is kept here. This class is serialized to JSON for the final
* output file.
*/
private static class NativeResourceInfo {
private String androidResourceId; // assigned by aapt, we got it from R.txt
private String stringsXmlPath; // relative path to the strings.xml where this
// resource originated from
public NativeResourceInfo(Integer androidResourceId, String stringsXmlPath) {
this.androidResourceId = String.format("0x%08X", androidResourceId);
this.stringsXmlPath = stringsXmlPath;
}
@SuppressWarnings("unused") // Used via reflection for JSON serialization.
public String getAndroidResourceId() {
return androidResourceId;
}
@SuppressWarnings("unused") // Used via reflection for JSON serialization.
public String getStringsXmlPath() {
return stringsXmlPath;
}
}
}