com.facebook.buck.android.StringResources.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.android.StringResources.java

Source

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