/* * Copyright 2013-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 com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.EnumMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * Represents string resources of types string, plural and array for a locale. Also responsible for * generating a custom format binary file for the resources. */ public class StringResources { /** * The values here need to be lowercase as we follow the standard in android xml files of using * lower case */ enum Gender { unknown, female, male } /** * Bump this whenever there's a change in the file format. The parser can decide to abort parsing * if the version it finds in the file does not match it's own version, thereby avoiding potential * data corruption issues. */ private static final int FORMAT_VERSION = 2; public final SortedMap<Integer, EnumMap<Gender, String>> strings; public final SortedMap<Integer, EnumMap<Gender, ImmutableMap<String, String>>> plurals; // This is not a TreeMultimap because we only want the keys to be sorted by their natural // ordering, not the values array. The values should be in the same order as insertion. public final SortedMap<Integer, EnumMap<Gender, ImmutableList<String>>> arrays; /** * These are the 6 fixed plural categories for string resources in Android. This mapping is not * expected to change over time. We encode them as integers to optimize space. * * <p>For more information, refer to: <a * href="http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">String * Resources | Android Developers </a> */ private static final ImmutableMap<String, Integer> PLURAL_CATEGORY_MAP = ImmutableMap.<String, Integer>builder() .put("zero", 0) .put("one", 1) .put("two", 2) .put("few", 3) .put("many", 4) .put("other", 5) .build(); private static Charset charset = Charsets.UTF_8; public StringResources( SortedMap<Integer, EnumMap<Gender, String>> strings, SortedMap<Integer, EnumMap<Gender, ImmutableMap<String, String>>> plurals, SortedMap<Integer, EnumMap<Gender, ImmutableList<String>>> arrays) { this.strings = strings; this.plurals = plurals; this.arrays = arrays; } public StringResources getMergedResources(StringResources otherResources) { TreeMap<Integer, EnumMap<Gender, String>> stringsMap = Maps.newTreeMap(otherResources.strings); TreeMap<Integer, EnumMap<Gender, ImmutableMap<String, String>>> pluralsMap = Maps.newTreeMap(otherResources.plurals); TreeMap<Integer, EnumMap<Gender, ImmutableList<String>>> arraysMap = Maps.newTreeMap(otherResources.arrays); stringsMap.putAll(strings); pluralsMap.putAll(plurals); arraysMap.putAll(arrays); return new StringResources(stringsMap, pluralsMap, arraysMap); } /** * Returns a byte array that represents the entire set of strings, plurals and string arrays in * the following binary file format: * * <p> * * <pre> * [Int: Version] * * [Int: # of strings] * [Int: Smallest resource id among strings] * [[Short: resource id delta] [Byte: #genders] [[Byte: gender enum ordinal] [Short: length of * the string]] x #genders] x # of strings * [Byte array of the string value] x # summation of genders over # of strings * * [Int: # of plurals] * [Int: Smallest resource id among plurals] * [[Short: resource id delta] [Byte: #genders] [[Byte: gender enum ordinal] [Byte: #categories] * [[Byte: category] [Short: length of plural value]] x #categories] x # of genders] * x # of plurals * [Byte array of plural value] x Summation of genders over plural categories over # of plurals * * [Int: # of arrays] * [Int: Smallest resource id among arrays] * [[Short: resource id delta] [Byte: #genders] [[Byte: gender enum ordinal] [Int: #elements] * [Short: length of element] x # phaof elements] x # of genders] x # of arrays * [Byte array of string value] x Summation of genders over array elements over # of arrays * </pre> */ public byte[] getBinaryFileContent() { try (ByteArrayOutputStream bytesStream = new ByteArrayOutputStream(); DataOutputStream outputStream = new DataOutputStream(bytesStream)) { outputStream.writeInt(FORMAT_VERSION); writeStrings(outputStream); writePlurals(outputStream); writeArrays(outputStream); return bytesStream.toByteArray(); } catch (IOException e) { // This should never happen since DataOutputStream redirects all calls to the underlying // stream and ByteArrayOutputStream does not throw IOException. throw new RuntimeException(e); } } /** * Writes the metadata and strings in the following format to the output stream: [Int: # of * strings] [Int: Smallest resource id among strings] [[Short: resource id delta] [Byte: #genders] * [[Byte: gender enum ordinal] [Short: length of the string] [Byte array of the string value]] x * #genders] x # of strings * * @param outputStream * @throws IOException */ private void writeStrings(DataOutputStream outputStream) throws IOException { outputStream.writeInt(strings.size()); if (strings.isEmpty()) { return; } int previousResourceId = strings.firstKey(); outputStream.writeInt(previousResourceId); try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) { for (Map.Entry<Integer, EnumMap<Gender, String>> entry : strings.entrySet()) { writeShort(outputStream, entry.getKey() - previousResourceId); EnumMap<Gender, String> genderMap = entry.getValue(); outputStream.writeByte(genderMap.size()); for (Map.Entry<Gender, String> gender : genderMap.entrySet()) { outputStream.writeByte(gender.getKey().ordinal()); byte[] genderValue = getUnescapedStringBytes(gender.getValue()); writeShort(outputStream, genderValue.length); dataStream.write(genderValue); } previousResourceId = entry.getKey(); } outputStream.write(dataStream.toByteArray()); } } /** * Writes the metadata and strings in the following format to the output stream: [Int: # of * plurals] [Int: Smallest resource id among plurals] [[Short: resource id delta] [Byte: #genders] * [[Byte: gender enum ordinal] [Byte: #categories] [[Byte: category] [Short: length of plural * value]] x #categories] x # of genders] x # of plurals [Byte array of plural value] x Summation * of gedners over plural categories over # of plurals * * @param outputStream * @throws IOException */ private void writePlurals(DataOutputStream outputStream) throws IOException { outputStream.writeInt(plurals.size()); if (plurals.isEmpty()) { return; } int previousResourceId = plurals.firstKey(); outputStream.writeInt(previousResourceId); try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) { for (Map.Entry<Integer, EnumMap<Gender, ImmutableMap<String, String>>> entry : plurals.entrySet()) { writeShort(outputStream, entry.getKey() - previousResourceId); EnumMap<Gender, ImmutableMap<String, String>> genderMap = entry.getValue(); outputStream.writeByte(genderMap.size()); for (Map.Entry<Gender, ImmutableMap<String, String>> gender : genderMap.entrySet()) { outputStream.writeByte(gender.getKey().ordinal()); ImmutableMap<String, String> categoryMap = gender.getValue(); outputStream.writeByte(categoryMap.size()); for (Map.Entry<String, String> cat : categoryMap.entrySet()) { outputStream.writeByte( Preconditions.checkNotNull(PLURAL_CATEGORY_MAP.get(cat.getKey())).byteValue()); byte[] pluralValue = getUnescapedStringBytes(cat.getValue()); writeShort(outputStream, pluralValue.length); dataStream.write(pluralValue); } } previousResourceId = entry.getKey(); } outputStream.write(dataStream.toByteArray()); } } /** * Writes the metadata and strings in the following format to the output stream: [Int: # of * arrays] [Int: Smallest resource id among arrays] [[Short: resource id delta] [Byte: #genders] * [[Byte: gender enum ordinal] [Int: #elements] [[Short: length of element] x # of elements]] x # * of genders] x # of arrays [Byte array of string value] x Summation of genders over array * elements over # of arrays * * @param outputStream * @throws IOException */ private void writeArrays(DataOutputStream outputStream) throws IOException { outputStream.writeInt(arrays.keySet().size()); if (arrays.keySet().isEmpty()) { return; } int previousResourceId = arrays.firstKey(); outputStream.writeInt(previousResourceId); try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) { for (Map.Entry<Integer, EnumMap<Gender, ImmutableList<String>>> entry : arrays.entrySet()) { writeShort(outputStream, entry.getKey() - previousResourceId); EnumMap<Gender, ImmutableList<String>> genderMap = entry.getValue(); outputStream.writeByte(genderMap.size()); for (Map.Entry<Gender, ImmutableList<String>> gender : genderMap.entrySet()) { outputStream.writeByte(gender.getKey().ordinal()); ImmutableList<String> arrayElements = gender.getValue(); outputStream.writeByte(arrayElements.size()); for (String arrayValue : arrayElements) { byte[] byteValue = getUnescapedStringBytes(arrayValue); writeShort(outputStream, byteValue.length); dataStream.write(byteValue); } } previousResourceId = entry.getKey(); } outputStream.write(dataStream.toByteArray()); } } private void writeShort(DataOutputStream stream, int number) throws IOException { Preconditions.checkState( number <= Short.MAX_VALUE, "Error attempting to compact a numeral to short: " + number); stream.writeShort(number); } @VisibleForTesting static byte[] getUnescapedStringBytes(String value) { return value.replace("\\\"", "\"").replace("\\'", "'").getBytes(charset); } }