/* * 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; } } }