FileDocCategorySizeDatePackage
AsmGenerator.javaAPI DocAndroid 1.5 API13521Wed May 06 22:42:02 BST 2009com.android.tools.layoutlib.create

AsmGenerator

public class AsmGenerator extends Object
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.

Fields Summary
private final Log
mLog
Output logger.
private final String
mOsDestJar
The path of the destination JAR to create.
private final Class[]
mInjectClasses
List of classes to inject in the final JAR from _this_ archive.
private final Set
mStubMethods
The set of methods to stub out.
private Map
mKeep
All classes to output as-is, except if they have native methods.
private Map
mDeps
All dependencies that must be completely stubbed.
private int
mRenameCount
Counter of number of classes renamed during transform.
private final HashMap
mRenameClasses
FQCN Names of the classes to rename: map old-FQCN => new-FQCN
private HashSet
mClassesNotRenamed
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 HashMap
mDeleteReturns
A map { FQCN => map { list of return types to delete from the FQCN } }.
Constructors Summary
public AsmGenerator(Log log, String osDestJar, Class[] injectClasses, String[] stubMethods, String[] renameClasses, String[] deleteReturns)
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
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.

        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<String>>();
        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));
            }
        }
    
Methods Summary
java.lang.StringbinaryToInternalClassName(java.lang.String className)
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.

        if (className == null) {
            return null;
        } else {
            return className.replace('.", '/");
        }
    
java.lang.StringclassNameToEntryPath(java.lang.String className)
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"

        return className.replaceAll("\\.", "/").concat(".class");
    
private java.lang.StringclassToEntryPath(java.lang.Class clazz)
Utility method to get the JAR entry path from a Class name. e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class"

        String name = "";
        Class<?> parent;
        while ((parent = clazz.getEnclosingClass()) != null) {
            name = "$" + clazz.getSimpleName() + name;
            clazz = parent;
        }
        return classNameToEntryPath(clazz.getCanonicalName() + name);        
    
voidcreateJar(java.io.FileOutputStream outStream, java.util.Map all)
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

        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();
    
public voidgenerate()
Generates the final JAR

        TreeMap<String, byte[]> all = new TreeMap<String, byte[]>();
        
        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);
    
public java.util.SetgetClassesNotRenamed()
Returns the list of classes that have not been renamed yet.

The names are "internal class names" rather than FQCN, i.e. they use "/" instead "." as package separators.

        return mClassesNotRenamed;
    
public java.util.MapgetDeps()
Gets the map of dependencies that must be completely stubbed

        return mDeps;
    
public java.util.MapgetKeep()
Gets the map of classes to output as-is, except if they have native methods

        return mKeep;
    
booleanhasNativeMethods(org.objectweb.asm.ClassReader cr)
Returns true if a class has any native methods.

        ClassHasNativeVisitor cv = new ClassHasNativeVisitor();
        cr.accept(cv, 0 /* flags */);
        return cv.hasNativeMethods();
    
public voidsetDeps(java.util.Map deps)
Sets the map of dependencies that must be completely stubbed

        mDeps = deps;
    
public voidsetKeep(java.util.Map keep)
Sets the map of classes to output as-is, except if they have native methods

        mKeep = keep;
    
byte[]transform(org.objectweb.asm.ClassReader cr, boolean stubNativesOnly)
Transforms a class.

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.)


        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();
    
java.lang.StringtransformName(java.lang.String className)
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 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;