com.facebook.buck.rules.MergeAndroidResourcesStep.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.rules.MergeAndroidResourcesStep.java

Source

/*
 * 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.rules;

import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.io.CharStreams;
import com.google.common.io.Closer;
import com.google.common.io.Files;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Scanner;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MergeAndroidResourcesStep implements Step {

    private static final Pattern TEXT_SYMBOLS_LINE = Pattern.compile("(\\S+) (\\S+) (\\S+) (.+)");

    private final ImmutableMap<String, String> symbolsFileToRDotJavaPackage;
    private final String pathToGeneratedJavaFiles;

    /**
     * Merges text symbols files from {@code aapt} into R.java files that can be compiled.
     * @param symbolsFileToRDotJavaPackage For each entry in the map, the key is a path to a symbols
     *     file generated by {@code aapt} using the {@code --output-text-symbols} flag. The value is
     *     the Java package for the corresponding R.java file.
     * @param pathToGeneratedJavaFiles the directory where the generated R.java files should be
     *     written. Admittedly, this command could write such files to a {@code /tmp} directory, but
     *     it is convenient to have the R.java files written to a known location for debugging. This
     *     directory should exist and be empty before this command is run.
     */
    public MergeAndroidResourcesStep(Map<String, String> symbolsFileToRDotJavaPackage,
            String pathToGeneratedJavaFiles) {
        this.symbolsFileToRDotJavaPackage = ImmutableMap.copyOf(symbolsFileToRDotJavaPackage);
        this.pathToGeneratedJavaFiles = Preconditions.checkNotNull(pathToGeneratedJavaFiles);
    }

    @Override
    public int execute(ExecutionContext context) {
        try {
            doExecute();
            return 0;
        } catch (IOException e) {
            e.printStackTrace(context.getStdErr());
            return 1;
        }
    }

    private void doExecute() throws IOException {
        // A symbols file may look like:
        //
        //    int id placeholder 0x7f020000
        //    int string debug_http_proxy_dialog_title 0x7f030004
        //    int string debug_http_proxy_hint 0x7f030005
        //    int string debug_http_proxy_summary 0x7f030003
        //    int string debug_http_proxy_title 0x7f030002
        //    int string debug_ssl_cert_check_summary 0x7f030001
        //    int string debug_ssl_cert_check_title 0x7f030000
        //
        // Note that there are four columns of information:
        // - the type of the resource id (always seems to be int or int[], in practice)
        // - the type of the resource
        // - the name of the resource
        // - the value of the resource id
        //
        // In order to convert this to R.java, all resources of the same type are grouped into a static
        // class of that name. The static class contains static values that correspond to the resource
        // (type, name, value) tuples.
        //
        // The first step is to merge symbol files of the same package type and resource type/name.
        // That is, within a package type, each resource type/name pair must be unique. If there are
        // multiple pairs, only one will be written to the R.java file.
        //
        // Because the resulting files do not match their respective resources.arsc, the values are
        // meaningless and do not represent the usable final result.  This is why the R.java file is
        // written without using final so that javac will not inline the values.  Unfortunately,
        // though Robolectric doesn't read resources.arsc, it does assert that all the R.java resource
        // ids are unique.  This forces us to re-enumerate new unique ids.
        SortedSetMultimap<String, Resource> rDotJavaPackageToResources = sortSymbols(
                new Function<String, Readable>() {
                    @Override
                    public Readable apply(String pathToFile) {
                        try {
                            return new FileReader(pathToFile);
                        } catch (FileNotFoundException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }, symbolsFileToRDotJavaPackage, true /* reenumerate */);

        // Create an R.java file for each package.
        for (String rDotJavaPackage : rDotJavaPackageToResources.keySet()) {
            // Create the content of R.java.
            SortedSet<Resource> resources = rDotJavaPackageToResources.get(rDotJavaPackage);

            // Write R.java in the pathToGeneratedJavaFiles directory. Admittedly, this will be written
            // to /tmp/com.example.stuff/R.java rather than /tmp/com/example/stuff/R.java. It turns out
            // that directory structure does not matter to javac.

            // Determine the path to R.java.
            File rDotJava = getOutputFile(pathToGeneratedJavaFiles, rDotJavaPackage);

            // Then write R.java to the output directory.
            Files.createParentDirs(rDotJava);
            BufferedWriter writer = Files.newWriter(rDotJava, Charsets.UTF_8);
            try {
                writeJavaCodeForPackageAndResources(new PrintWriter(writer), rDotJavaPackage, resources);
            } finally {
                writer.close();
            }
        }
    }

    @VisibleForTesting
    static SortedSetMultimap<String, Resource> sortSymbols(Function<String, Readable> filePathToReadable,
            Map<String, String> symbolsFileToRDotJavaPackage, boolean reenumerate) {
        // If we're reenumerating, start at 0x7f01001 so that the resulting file is human readable.
        // This value range (0x7f010001 - ...) is easier to spot as an actual resource id instead of
        // other values in styleable which can be enumerated integers starting at 0.
        IntEnumerator enumerator = reenumerate ? new IntEnumerator(0x7f01001) : null;
        SortedSetMultimap<String, Resource> rDotJavaPackageToSymbolsFiles = TreeMultimap.create();
        for (Map.Entry<String, String> entry : symbolsFileToRDotJavaPackage.entrySet()) {
            String symbolsFile = entry.getKey();
            String packageName = entry.getValue();

            // Read the symbols file and parse each line as a Resource.
            Readable readable = filePathToReadable.apply(symbolsFile);
            Scanner scanner = new Scanner(readable);
            while (scanner.hasNext()) {
                String line = scanner.nextLine();
                Matcher matcher = TEXT_SYMBOLS_LINE.matcher(line);
                boolean isMatch = matcher.matches();
                Preconditions.checkState(isMatch, "Should be able to match '%s'.", line);
                String idType = matcher.group(1);
                String type = matcher.group(2);
                String name = matcher.group(3);
                String idValue = matcher.group(4);

                // We're only doing the remapping so Roboelectric is happy and it is already ignoring the
                // id references found in the styleable section.  So let's do that as well so we don't have
                // to get fancier than is needed.  That is, just re-enumerate all app-level resource ids
                // and ignore everything else, allowing the styleable references to be messed up.
                String idValueToUse = idValue;
                if (reenumerate && idValue.startsWith("0x7f")) {
                    idValueToUse = String.format("0x%08x", enumerator.next());
                }

                Resource resource = new Resource(idType, type, name, idValue, idValueToUse);
                rDotJavaPackageToSymbolsFiles.put(packageName, resource);
            }
        }
        return rDotJavaPackageToSymbolsFiles;
    }

    public static String generateJavaCodeForPackageWithoutResources(String packageName) {
        return generateJavaCodeForPackageAndResources(packageName, ImmutableSortedSet.<Resource>of());
    }

    public static String generateJavaCodeForPackageAndResources(String packageName, SortedSet<Resource> resources) {
        StringBuilder b = new StringBuilder();
        Closer closer = Closer.create();
        PrintWriter writer = closer.register(new PrintWriter(CharStreams.asWriter(b)));
        try {
            writeJavaCodeForPackageAndResources(writer, packageName, resources);
        } catch (IOException e) {
            // Impossible.
            throw new RuntimeException(e);
        } finally {
            try {
                closer.close();
            } catch (IOException e) {
                Throwables.propagate(e);
            }
        }
        return b.toString();
    }

    /**
     * Writes an intermediate R.java with dummy values influenced by the also dummy values created by
     * {@code aapt} when building intermediate artifacts.
     *
     * @param writer Output writer for the Java source.
     * @param packageName Package of the resulting R.java file.
     * @param resources Sorted set of resources parsed from R.txt.  First sorted by type then name.
     */
    private static void writeJavaCodeForPackageAndResources(PrintWriter writer, String packageName,
            SortedSet<Resource> resources) throws IOException {
        Preconditions.checkNotNull(writer);
        Preconditions.checkNotNull(packageName);
        Preconditions.checkNotNull(resources);

        writer.append("package ").append(packageName).append(';').println();
        writer.println();
        writer.println("public class R {");
        writer.println();

        String lastType = null;
        for (Resource res : resources) {
            String type = res.type;
            if (!type.equals(lastType)) {
                // If the previous type needs, to be closed, then close it.
                if (lastType != null) {
                    writer.println("  }");
                    writer.println();
                }

                // Now start the block for the new type.
                writer.append("  public static class ").append(type).append(" {").println();
                lastType = type;
            }

            // Write out the resource.
            // Write as an int.
            writer.println(
                    String.format("    public static final %s %s=%s;", res.idType, res.name, res.idValueToWrite));
        }

        // If some type was written (e.g., the for loop was entered), then the last type needs to be
        // closed.
        if (lastType != null) {
            writer.println("  }");
            writer.println();
        }

        // Close the class definition.
        writer.println("}");
    }

    public static String getOutputFilePath(String pathToGeneratedJavaFiles, String rDotJavaPackage) {
        return getOutputFile(pathToGeneratedJavaFiles, rDotJavaPackage).getPath();
    }

    private static File getOutputFile(String pathToGeneratedJavaFiles, String rDotJavaPackage) {
        File outputDir = new File(pathToGeneratedJavaFiles, rDotJavaPackage);
        File rDotJava = new File(outputDir, "R.java");
        return rDotJava;
    }

    /** Represents a row from a symbols file generated by {@code aapt}. */
    @VisibleForTesting
    static class Resource implements Comparable<Resource> {
        @VisibleForTesting
        final String idType;
        @VisibleForTesting
        final String type;
        @VisibleForTesting
        final String name;
        @VisibleForTesting
        final String originalIdValue;
        @VisibleForTesting
        final String idValueToWrite;

        public Resource(String idType, String type, String name, String originalIdValue, String idValueToWrite) {
            this.idType = Preconditions.checkNotNull(idType);
            this.type = Preconditions.checkNotNull(type);
            this.name = Preconditions.checkNotNull(name);
            this.originalIdValue = Preconditions.checkNotNull(originalIdValue);
            this.idValueToWrite = Preconditions.checkNotNull(idValueToWrite);
        }

        /**
         * A collection of Resources should be sorted such that Resources of the same type should be
         * grouped together, and should be alphabetized within that group.
         */
        @Override
        public int compareTo(Resource that) {
            return ComparisonChain.start().compare(this.type, that.type).compare(this.name, that.name).result();
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Resource)) {
                return false;
            }

            Resource that = (Resource) obj;
            return Objects.equal(this.type, that.type) && Objects.equal(this.name, that.name);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(type, name);
        }

        @Override
        public String toString() {
            return Objects.toStringHelper(Resource.class).add("idType", idType).add("type", type).add("name", name)
                    .add("originalIdValue", originalIdValue).add("idValueToWrite", idValueToWrite).toString();
        }
    }

    @Override
    public String getShortName(ExecutionContext context) {
        return "android-res-merge";
    }

    @Override
    public String getDescription(ExecutionContext context) {
        return getShortName(context) + " " + Joiner.on(' ').join(symbolsFileToRDotJavaPackage.keySet());
    }

    private static class IntEnumerator {
        private int value;

        public IntEnumerator(int start) {
            value = start;
        }

        public int next() {
            Preconditions.checkState(value < Integer.MAX_VALUE, "Stop goofing off");
            return value++;
        }
    }

}