com.google.devtools.build.android.resources.RClassGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.android.resources.RClassGenerator.java

Source

// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.android.resources;

import com.android.SdkConstants;
import com.android.builder.internal.SymbolLoader;
import com.android.builder.internal.SymbolLoader.SymbolEntry;
import com.android.resources.ResourceType;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.InstructionAdapter;

/**
 * Writes out bytecode for an R.class directly, rather than go through an R.java and compile. This
 * avoids re-parsing huge R.java files and other time spent in the java compiler (e.g., plugins like
 * ErrorProne). A difference is that this doesn't generate line number tables and other debugging
 * information. Also, the order of the constant pool tends to be different.
 */
public class RClassGenerator {

    private static final Logger logger = Logger.getLogger(RClassGenerator.class.getName());
    private static final int JAVA_VERSION = Opcodes.V1_7;
    private static final String SUPER_CLASS = "java/lang/Object";
    private final Path outFolder;
    private final String packageName;
    private final Map<ResourceType, List<FieldInitializer>> initializers;
    private final boolean finalFields;
    private static final Splitter PACKAGE_SPLITTER = Splitter.on('.');

    /**
     * Create an RClassGenerator given the final binary's symbol values, and a collection of symbols
     * for the given package.
     *
     * @param outFolder base folder to place the output R class files.
     * @param packageName the java package to use for the R class
     * @param values the final symbol values (may include more symbols than needed for this package)
     * @param packageSymbols the symbols in this package
     * @param finalFields true if the fields should be marked final
     */
    public static RClassGenerator fromSymbols(Path outFolder, String packageName, SymbolLoader values,
            Collection<SymbolLoader> packageSymbols, boolean finalFields) throws IOException {
        Table<String, String, SymbolEntry> symbolsTable = getAllSymbols(packageSymbols);
        Table<String, String, SymbolEntry> valuesTable = getSymbols(values);
        Map<ResourceType, List<FieldInitializer>> initializers = getInitializers(symbolsTable, valuesTable);
        return new RClassGenerator(outFolder, packageName, initializers, finalFields);
    }

    /**
     * Create an RClassGenerator given a collection of initializers.
     *
     * @param outFolder base folder to place the output R class files.
     * @param packageName the java package to use for the R class
     * @param initializers the list of initializers to use for each inner class
     * @param finalFields true if the fields should be marked final
     */
    public RClassGenerator(Path outFolder, String packageName,
            Map<ResourceType, List<FieldInitializer>> initializers, boolean finalFields) {
        this.outFolder = outFolder;
        this.packageName = packageName;
        this.finalFields = finalFields;
        this.initializers = initializers;
    }

    private static Table<String, String, SymbolEntry> getAllSymbols(Collection<SymbolLoader> symbolLoaders)
            throws IOException {
        Table<String, String, SymbolEntry> symbols = HashBasedTable.create();
        for (SymbolLoader symbolLoader : symbolLoaders) {
            symbols.putAll(getSymbols(symbolLoader));
        }
        return symbols;
    }

    private static Table<String, String, SymbolEntry> getSymbols(SymbolLoader symbolLoader) throws IOException {
        // TODO(bazel-team): remove when we update android_ide_common to a version w/ public visibility
        try {
            Method getSymbols = SymbolLoader.class.getDeclaredMethod("getSymbols");
            getSymbols.setAccessible(true);
            @SuppressWarnings("unchecked")
            Table<String, String, SymbolEntry> result = (Table<String, String, SymbolEntry>) getSymbols
                    .invoke(symbolLoader);
            return result;
        } catch (ReflectiveOperationException e) {
            throw new IOException(e);
        }
    }

    /** Convert the {@link SymbolLoader} data, to a map of {@link FieldInitializer}. */
    private static Map<ResourceType, List<FieldInitializer>> getInitializers(
            Table<String, String, SymbolEntry> symbols, Table<String, String, SymbolEntry> values) {
        Map<ResourceType, List<FieldInitializer>> initializers = new EnumMap<>(ResourceType.class);
        for (String typeName : symbols.rowKeySet()) {
            ResourceType resourceType = ResourceType.getEnum(typeName);
            Preconditions.checkNotNull(resourceType);
            initializers.put(resourceType, getInitializers(typeName, symbols, values));
        }
        return initializers;
    }

    private static List<FieldInitializer> getInitializers(String typeName,
            Table<String, String, SymbolEntry> symbols, Table<String, String, SymbolEntry> values) {
        Map<String, SymbolEntry> rowMap = symbols.row(typeName);
        Set<String> symbolSet = rowMap.keySet();
        List<String> symbolList = new ArrayList<>(symbolSet);
        Collections.sort(symbolList);
        List<FieldInitializer> initializers = new ArrayList<>();
        for (String symbolName : symbolList) {
            // get the matching SymbolEntry from the values Table.
            SymbolEntry value = values.get(typeName, symbolName);
            if (value != null) {
                if (value.getType().equals("int")) {
                    initializers.add(IntFieldInitializer.of(value.getName(), value.getValue()));
                } else {
                    Preconditions.checkArgument(value.getType().equals("int[]"));
                    initializers.add(IntArrayFieldInitializer.of(value.getName(), value.getValue()));
                }
            } else {
                // Value may be missing if resource overriding eliminates resources at the binary
                // level, which were originally present at the library level.
                logger.fine(String.format("Skipping R.%s.%s -- value not known in binary's R.txt", typeName,
                        symbolName));
            }
        }
        return initializers;
    }

    /** Builds the bytecode and writes out the R.class file, and R$inner.class files. */
    public void write() throws IOException {
        Iterable<String> folders = PACKAGE_SPLITTER.split(packageName);
        Path packageDir = outFolder;
        for (String folder : folders) {
            packageDir = packageDir.resolve(folder);
        }
        // At least create the outFolder that was requested. However, if there are no symbols, don't
        // create the R.class and inner class files (no need to have an empty class).
        Files.createDirectories(packageDir);
        if (initializers.isEmpty()) {
            return;
        }
        Path rClassFile = packageDir.resolve(SdkConstants.FN_COMPILED_RESOURCE_CLASS);

        String packageWithSlashes = packageName.replaceAll("\\.", "/");
        String rClassName = packageWithSlashes.isEmpty() ? "R" : (packageWithSlashes + "/R");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        classWriter.visit(JAVA_VERSION, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER, rClassName,
                null, /* signature */
                SUPER_CLASS, null /* interfaces */);
        classWriter.visitSource(SdkConstants.FN_RESOURCE_CLASS, null);
        writeConstructor(classWriter);

        // Build the R.class w/ the inner classes, then later build the individual R$inner.class.
        for (ResourceType resourceType : initializers.keySet()) {
            String innerClassName = rClassName + "$" + resourceType;
            classWriter.visitInnerClass(innerClassName, rClassName, resourceType.toString(),
                    Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC);
        }
        classWriter.visitEnd();
        Files.write(rClassFile, classWriter.toByteArray());

        // Now generate the R$inner.class files.
        for (Map.Entry<ResourceType, List<FieldInitializer>> entry : initializers.entrySet()) {
            writeInnerClass(entry.getValue(), packageDir, rClassName, entry.getKey().toString());
        }
    }

    private void writeInnerClass(List<FieldInitializer> initializers, Path packageDir,
            String fullyQualifiedOuterClass, String innerClass) throws IOException {
        ClassWriter innerClassWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        String fullyQualifiedInnerClass = writeInnerClassHeader(fullyQualifiedOuterClass, innerClass,
                innerClassWriter);

        List<FieldInitializer> deferredInitializers = new ArrayList<>();
        int fieldAccessLevel = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC;
        if (finalFields) {
            fieldAccessLevel |= Opcodes.ACC_FINAL;
        }
        for (FieldInitializer init : initializers) {
            if (init.writeFieldDefinition(innerClassWriter, fieldAccessLevel, finalFields)) {
                deferredInitializers.add(init);
            }
        }
        if (!deferredInitializers.isEmpty()) {
            writeStaticClassInit(innerClassWriter, fullyQualifiedInnerClass, deferredInitializers);
        }

        innerClassWriter.visitEnd();
        Path innerFile = packageDir.resolve("R$" + innerClass + ".class");
        Files.write(innerFile, innerClassWriter.toByteArray());
    }

    private String writeInnerClassHeader(String fullyQualifiedOuterClass, String innerClass,
            ClassWriter innerClassWriter) {
        String fullyQualifiedInnerClass = fullyQualifiedOuterClass + "$" + innerClass;
        innerClassWriter.visit(JAVA_VERSION, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER,
                fullyQualifiedInnerClass, null, /* signature */
                SUPER_CLASS, null /* interfaces */);
        innerClassWriter.visitSource(SdkConstants.FN_RESOURCE_CLASS, null);
        writeConstructor(innerClassWriter);
        innerClassWriter.visitInnerClass(fullyQualifiedInnerClass, fullyQualifiedOuterClass, innerClass,
                Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC);
        return fullyQualifiedInnerClass;
    }

    private static void writeConstructor(ClassWriter classWriter) {
        MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null,
                /* signature */ null /* exceptions */);
        constructor.visitCode();
        constructor.visitVarInsn(Opcodes.ALOAD, 0);
        constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, SUPER_CLASS, "<init>", "()V", false);
        constructor.visitInsn(Opcodes.RETURN);
        constructor.visitMaxs(1, 1);
        constructor.visitEnd();
    }

    private static void writeStaticClassInit(ClassWriter classWriter, String className,
            List<FieldInitializer> initializers) {
        MethodVisitor visitor = classWriter.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null,
                /* signature */ null /* exceptions */);
        visitor.visitCode();
        int stackSlotsNeeded = 0;
        InstructionAdapter insts = new InstructionAdapter(visitor);
        for (FieldInitializer fieldInit : initializers) {
            stackSlotsNeeded = Math.max(stackSlotsNeeded, fieldInit.writeCLInit(insts, className));
        }
        insts.areturn(Type.VOID_TYPE);
        visitor.visitMaxs(stackSlotsNeeded, 0);
        visitor.visitEnd();
    }
}