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