Java tutorial
/* * 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.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.TreeMultimap; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collection; import java.util.Map; 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 { /** * 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 = 1; public final TreeMap<Integer, String> strings; public final TreeMap<Integer, ImmutableMap<String, String>> plurals; public final TreeMultimap<Integer, 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></p> */ 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(TreeMap<Integer, String> strings, TreeMap<Integer, ImmutableMap<String, String>> plurals, TreeMultimap<Integer, String> arrays) { this.strings = Preconditions.checkNotNull(strings); this.plurals = Preconditions.checkNotNull(plurals); this.arrays = Preconditions.checkNotNull(arrays); } public StringResources getMergedResources(StringResources otherResources) { TreeMap<Integer, String> stringsMap = Maps.newTreeMap(otherResources.strings); TreeMap<Integer, ImmutableMap<String, String>> pluralsMap = Maps.newTreeMap(otherResources.plurals); TreeMultimap<Integer, String> arraysMap = TreeMultimap.create(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][Short: length of the string] x # of strings * [Byte array of the string value] x # of strings * [Int: # of plurals] * [Int: Smallest resource id among plurals] * [[Short: resource id delta][Byte: #categories][[Byte: category][Short: length of plural * value]] x #categories] x # of plurals * [Byte array of plural value] x Summation of plural categories over # of plurals * [Int: # of arrays] * [Int: Smallest resource id among arrays] * [[Short: resource id delta][Int: #elements][Short: length of element] x #elements] x # of * arrays * [Byte array of string value] x Summation of array elements over # of arrays * </pre> * </p> */ 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) { return null; } } 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, String> entry : strings.entrySet()) { byte[] resourceBytes = getUnescapedStringBytes(entry.getValue()); writeShort(outputStream, entry.getKey() - previousResourceId); writeShort(outputStream, resourceBytes.length); dataStream.write(resourceBytes, 0, resourceBytes.length); previousResourceId = entry.getKey(); } outputStream.write(dataStream.toByteArray()); } } 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, ImmutableMap<String, String>> entry : plurals.entrySet()) { writeShort(outputStream, entry.getKey() - previousResourceId); ImmutableMap<String, String> categoryMap = entry.getValue(); outputStream.writeByte(categoryMap.size()); for (Map.Entry<String, String> cat : categoryMap.entrySet()) { outputStream.writeByte(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()); } } private void writeArrays(DataOutputStream outputStream) throws IOException { outputStream.writeInt(arrays.keySet().size()); if (arrays.keySet().isEmpty()) { return; } int previousResourceId = arrays.keySet().first(); outputStream.writeInt(previousResourceId); try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) { for (int resourceId : arrays.keySet()) { writeShort(outputStream, resourceId - previousResourceId); Collection<String> arrayValues = arrays.get(resourceId); outputStream.writeInt(arrayValues.size()); for (String arrayValue : arrayValues) { byte[] byteValue = getUnescapedStringBytes(arrayValue); writeShort(outputStream, byteValue.length); dataStream.write(byteValue); } previousResourceId = resourceId; } 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); } }