alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Android example source code file (AsmGenerator.java)

This example Android source code file (AsmGenerator.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Android by Example" TM.

Java - Android tags/keywords

asmgenerator, classhasnativevisitor, classreader, entry, hashmap, hashset, io, ioexception, jar, jarentry, jaroutputstream, map, set, string, transformclassadapter, treemap, util

The AsmGenerator.java Android example source code

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * 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.android.tools.layoutlib.create;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

/**
 * Class that generates a new JAR from a list of classes, some of which are to be kept as-is
 * and some of which are to be stubbed partially or totally.
 */
public class AsmGenerator {

    /** Output logger. */
    private final Log mLog;
    /** The path of the destination JAR to create. */
    private final String mOsDestJar;
    /** List of classes to inject in the final JAR from _this_ archive. */
    private final Class<?>[] mInjectClasses;
    /** The set of methods to stub out. */
    private final Set<String> mStubMethods;
    /** All classes to output as-is, except if they have native methods. */
    private Map<String, ClassReader> mKeep;
    /** All dependencies that must be completely stubbed. */
    private Map<String, ClassReader> mDeps;
    /** Counter of number of classes renamed during transform. */
    private int mRenameCount;
    /** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */
    private final HashMap<String, String> mRenameClasses;
    /** FQCN Names of "old" classes that were NOT renamed. This starts with the full list of
     *  old-FQCN to rename and they get erased as they get renamed. At the end, classes still
     *  left here are not in the code base anymore and thus were not renamed. */
    private HashSet<String> mClassesNotRenamed;
    /** A map { FQCN => map { list of return types to delete from the FQCN } }. */
    private HashMap<String, Set mDeleteReturns;

    /**
     * Creates a new generator that can generate the output JAR with the stubbed classes.
     * 
     * @param log Output logger.
     * @param osDestJar The path of the destination JAR to create.
     * @param injectClasses The list of class from layoutlib_create to inject in layoutlib.
     * @param stubMethods The list of methods to stub out. Each entry must be in the form
     *          "package.package.OuterClass$InnerClass#MethodName".
     * @param renameClasses The list of classes to rename, must be an even list: the binary FQCN
     *          of class to replace followed by the new FQCN.
     * @param deleteReturns List of classes for which the methods returning them should be deleted.
     * The array contains a list of null terminated section starting with the name of the class
     * to rename in which the methods are deleted, followed by a list of return types identifying
     * the methods to delete.
     */
    public AsmGenerator(Log log, String osDestJar,
            Class<?>[] injectClasses,
            String[] stubMethods,
            String[] renameClasses, String[] deleteReturns) {
        mLog = log;
        mOsDestJar = osDestJar;
        mInjectClasses = injectClasses != null ? injectClasses : new Class<?>[0];
        mStubMethods = stubMethods != null ? new HashSet<String>(Arrays.asList(stubMethods)) :
                                             new HashSet<String>();

        // Create the map of classes to rename.
        mRenameClasses = new HashMap<String, String>();
        mClassesNotRenamed = new HashSet<String>();
        int n = renameClasses == null ? 0 : renameClasses.length;
        for (int i = 0; i < n; i += 2) {
            assert i + 1 < n;
            // The ASM class names uses "/" separators, whereas regular FQCN use "."
            String oldFqcn = binaryToInternalClassName(renameClasses[i]);
            String newFqcn = binaryToInternalClassName(renameClasses[i + 1]);
            mRenameClasses.put(oldFqcn, newFqcn);
            mClassesNotRenamed.add(oldFqcn);
        }
        
        // create the map of renamed class -> return type of method to delete.
        mDeleteReturns = new HashMap<String, Set();
        if (deleteReturns != null) {
            Set<String> returnTypes = null;
            String renamedClass = null;
            for (String className : deleteReturns) {
                // if we reach the end of a section, add it to the main map
                if (className == null) {
                    if (returnTypes != null) {
                        mDeleteReturns.put(renamedClass, returnTypes);
                    }
                    
                    renamedClass = null;
                    continue;
                }
    
                // if the renamed class is null, this is the beginning of a section
                if (renamedClass == null) {
                    renamedClass = binaryToInternalClassName(className);
                    continue;
                }
    
                // just a standard return type, we add it to the list.
                if (returnTypes == null) {
                    returnTypes = new HashSet<String>();
                }
                returnTypes.add(binaryToInternalClassName(className));
            }
        }
    }
    
    /**
     * Returns the list of classes that have not been renamed yet.
     * <p/>
     * The names are "internal class names" rather than FQCN, i.e. they use "/" instead "."
     * as package separators.
     */
    public Set<String> getClassesNotRenamed() {
        return mClassesNotRenamed;
    }

    /**
     * Utility that returns the internal ASM class name from a fully qualified binary class
     * name. E.g. it returns android/view/View from android.view.View.
     */
    String binaryToInternalClassName(String className) {
        if (className == null) {
            return null;
        } else {
            return className.replace('.', '/');
        }
    }

    /** Sets the map of classes to output as-is, except if they have native methods */
    public void setKeep(Map<String, ClassReader> keep) {
        mKeep = keep;
    }

    /** Sets the map of dependencies that must be completely stubbed */
    public void setDeps(Map<String, ClassReader> deps) {
        mDeps = deps;
    }
    
    /** Gets the map of classes to output as-is, except if they have native methods */
    public Map<String, ClassReader> getKeep() {
        return mKeep;
    }
    
    /** Gets the map of dependencies that must be completely stubbed */
    public Map<String, ClassReader> getDeps() {
        return mDeps;
    }

    /** Generates the final JAR */
    public void generate() throws FileNotFoundException, IOException {
        TreeMap<String, byte[]> all = new TreeMap();
        
        for (Class<?> clazz : mInjectClasses) {
            String name = classToEntryPath(clazz);
            InputStream is = ClassLoader.getSystemResourceAsStream(name);
            ClassReader cr = new ClassReader(is);
            byte[] b = transform(cr, true /* stubNativesOnly */);
            name = classNameToEntryPath(transformName(cr.getClassName()));
            all.put(name, b);
        }
        
        for (Entry<String, ClassReader> entry : mDeps.entrySet()) {
            ClassReader cr = entry.getValue();
            byte[] b = transform(cr, true /* stubNativesOnly */);
            String name = classNameToEntryPath(transformName(cr.getClassName()));
            all.put(name, b);
        }

        for (Entry<String, ClassReader> entry : mKeep.entrySet()) {
            ClassReader cr = entry.getValue();
            byte[] b = transform(cr, true /* stubNativesOnly */);
            String name = classNameToEntryPath(transformName(cr.getClassName()));
            all.put(name, b);
        }

        mLog.info("# deps classes: %d", mDeps.size());
        mLog.info("# keep classes: %d", mKeep.size());
        mLog.info("# renamed     : %d", mRenameCount);

        createJar(new FileOutputStream(mOsDestJar), all);
        mLog.info("Created JAR file %s", mOsDestJar);
    }

    /**
     * Writes the JAR file.
     * 
     * @param outStream The file output stream were to write the JAR. 
     * @param all The map of all classes to output.
     * @throws IOException if an I/O error has occurred
     */
    void createJar(FileOutputStream outStream, Map<String,byte[]> all) throws IOException {
        JarOutputStream jar = new JarOutputStream(outStream);
        for (Entry<String, byte[]> entry : all.entrySet()) {
            String name = entry.getKey();
            JarEntry jar_entry = new JarEntry(name);
            jar.putNextEntry(jar_entry);
            jar.write(entry.getValue());
            jar.closeEntry();
        }
        jar.flush();
        jar.close();
    }

    /**
     * Utility method that converts a fully qualified java name into a JAR entry path
     * e.g. for the input "android.view.View" it returns "android/view/View.class"
     */
    String classNameToEntryPath(String className) {
        return className.replaceAll("\\.", "/").concat(".class");
    }
    
    /**
     * Utility method to get the JAR entry path from a Class name.
     * e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class"
     */
    private String classToEntryPath(Class<?> clazz) {
        String name = "";
        Class<?> parent;
        while ((parent = clazz.getEnclosingClass()) != null) {
            name = "$" + clazz.getSimpleName() + name;
            clazz = parent;
        }
        return classNameToEntryPath(clazz.getCanonicalName() + name);        
    }

    /**
     * Transforms a class.
     * <p/>
     * There are 3 kind of transformations:
     * 
     * 1- For "mock" dependencies classes, we want to remove all code from methods and replace
     * by a stub. Native methods must be implemented with this stub too. Abstract methods are
     * left intact. Modified classes must be overridable (non-private, non-final).
     * Native methods must be made non-final, non-private.
     * 
     * 2- For "keep" classes, we want to rewrite all native methods as indicated above.
     * If a class has native methods, it must also be made non-private, non-final.
     * 
     * Note that unfortunately static methods cannot be changed to non-static (since static and
     * non-static are invoked differently.)
     */
    byte[] transform(ClassReader cr, boolean stubNativesOnly) {

        boolean hasNativeMethods = hasNativeMethods(cr);
        String className = cr.getClassName();
        
        String newName = transformName(className);
        // transformName returns its input argument if there's no need to rename the class
        if (newName != className) {
            mRenameCount++;
            // This class is being renamed, so remove it from the list of classes not renamed.
            mClassesNotRenamed.remove(className);
        }

        mLog.debug("Transform %s%s%s%s", className,
                newName == className ? "" : " (renamed to " + newName + ")",
                hasNativeMethods ? " -- has natives" : "",
                stubNativesOnly ? " -- stub natives only" : "");

        // Rewrite the new class from scratch, without reusing the constant pool from the
        // original class reader.
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        
        ClassVisitor rv = cw;
        if (newName != className) {
            rv = new RenameClassAdapter(cw, className, newName);
        }
        
        TransformClassAdapter cv = new TransformClassAdapter(mLog, mStubMethods, 
                mDeleteReturns.get(className),
                newName, rv,
                stubNativesOnly, stubNativesOnly || hasNativeMethods);
        cr.accept(cv, 0 /* flags */);
        return cw.toByteArray();
    }

    /**
     * Should this class be renamed, this returns the new name. Otherwise it returns the
     * original name.
     *
     * @param className The internal ASM name of the class that may have to be renamed
     * @return A new transformed name or the original input argument.
     */
    String transformName(String className) {
        String newName = mRenameClasses.get(className);
        if (newName != null) {
            return newName;
        }
        int pos = className.indexOf('$');
        if (pos > 0) {
            // Is this an inner class of a renamed class?
            String base = className.substring(0, pos);
            newName = mRenameClasses.get(base);
            if (newName != null) {
                return newName + className.substring(pos);
            }
        }
        
        return className;
    }

    /**
     * Returns true if a class has any native methods.
     */
    boolean hasNativeMethods(ClassReader cr) {
        ClassHasNativeVisitor cv = new ClassHasNativeVisitor();
        cr.accept(cv, 0 /* flags */);
        return cv.hasNativeMethods();
    }

}

Other Android examples (source code examples)

Here is a short list of links related to this Android AsmGenerator.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.