AsmGenerator.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.tools.layoutlib.create; 18 19import org.objectweb.asm.ClassReader; 20import org.objectweb.asm.ClassVisitor; 21import org.objectweb.asm.ClassWriter; 22 23import java.io.FileNotFoundException; 24import java.io.FileOutputStream; 25import java.io.IOException; 26import java.io.InputStream; 27import java.util.Arrays; 28import java.util.HashMap; 29import java.util.HashSet; 30import java.util.Map; 31import java.util.Set; 32import java.util.TreeMap; 33import java.util.Map.Entry; 34import java.util.jar.JarEntry; 35import java.util.jar.JarOutputStream; 36 37/** 38 * Class that generates a new JAR from a list of classes, some of which are to be kept as-is 39 * and some of which are to be stubbed partially or totally. 40 */ 41public class AsmGenerator { 42 43 /** Output logger. */ 44 private final Log mLog; 45 /** The path of the destination JAR to create. */ 46 private final String mOsDestJar; 47 /** List of classes to inject in the final JAR from _this_ archive. */ 48 private final Class<?>[] mInjectClasses; 49 /** The set of methods to stub out. */ 50 private final Set<String> mStubMethods; 51 /** All classes to output as-is, except if they have native methods. */ 52 private Map<String, ClassReader> mKeep; 53 /** All dependencies that must be completely stubbed. */ 54 private Map<String, ClassReader> mDeps; 55 /** Counter of number of classes renamed during transform. */ 56 private int mRenameCount; 57 /** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */ 58 private final HashMap<String, String> mRenameClasses; 59 /** FQCN Names of "old" classes that were NOT renamed. This starts with the full list of 60 * old-FQCN to rename and they get erased as they get renamed. At the end, classes still 61 * left here are not in the code base anymore and thus were not renamed. */ 62 private HashSet<String> mClassesNotRenamed; 63 /** A map { FQCN => map { list of return types to delete from the FQCN } }. */ 64 private HashMap<String, Set<String>> mDeleteReturns; 65 66 /** 67 * Creates a new generator that can generate the output JAR with the stubbed classes. 68 * 69 * @param log Output logger. 70 * @param osDestJar The path of the destination JAR to create. 71 * @param stubMethods The list of methods to stub out 72 * @param renameClasses The list of classes to rename, must be an even list: the binary FQCN 73 * of class to replace followed by the new FQCN. 74 * @param deleteReturns List of classes for which the methods returning them should be deleted. 75 * The array contains a list of null terminated section starting with the name of the class 76 * to rename in which the methods are deleted, followed by a list of return types identifying 77 * the methods to delete. 78 */ 79 public AsmGenerator(Log log, String osDestJar, 80 Class<?>[] injectClasses, 81 String[] stubMethods, 82 String[] renameClasses, String[] deleteReturns) { 83 mLog = log; 84 mOsDestJar = osDestJar; 85 mInjectClasses = injectClasses != null ? injectClasses : new Class<?>[0]; 86 mStubMethods = stubMethods != null ? new HashSet<String>(Arrays.asList(stubMethods)) : 87 new HashSet<String>(); 88 89 // Create the map of classes to rename. 90 mRenameClasses = new HashMap<String, String>(); 91 mClassesNotRenamed = new HashSet<String>(); 92 int n = renameClasses == null ? 0 : renameClasses.length; 93 for (int i = 0; i < n; i += 2) { 94 assert i + 1 < n; 95 // The ASM class names uses "/" separators, whereas regular FQCN use "." 96 String oldFqcn = binaryToInternalClassName(renameClasses[i]); 97 String newFqcn = binaryToInternalClassName(renameClasses[i + 1]); 98 mRenameClasses.put(oldFqcn, newFqcn); 99 mClassesNotRenamed.add(oldFqcn); 100 } 101 102 // create the map of renamed class -> return type of method to delete. 103 mDeleteReturns = new HashMap<String, Set<String>>(); 104 if (deleteReturns != null) { 105 Set<String> returnTypes = null; 106 String renamedClass = null; 107 for (String className : deleteReturns) { 108 // if we reach the end of a section, add it to the main map 109 if (className == null) { 110 if (returnTypes != null) { 111 mDeleteReturns.put(renamedClass, returnTypes); 112 } 113 114 renamedClass = null; 115 continue; 116 } 117 118 // if the renamed class is null, this is the beginning of a section 119 if (renamedClass == null) { 120 renamedClass = binaryToInternalClassName(className); 121 continue; 122 } 123 124 // just a standard return type, we add it to the list. 125 if (returnTypes == null) { 126 returnTypes = new HashSet<String>(); 127 } 128 returnTypes.add(binaryToInternalClassName(className)); 129 } 130 } 131 } 132 133 /** 134 * Returns the list of classes that have not been renamed yet. 135 * <p/> 136 * The names are "internal class names" rather than FQCN, i.e. they use "/" instead "." 137 * as package separators. 138 */ 139 public Set<String> getClassesNotRenamed() { 140 return mClassesNotRenamed; 141 } 142 143 /** 144 * Utility that returns the internal ASM class name from a fully qualified binary class 145 * name. E.g. it returns android/view/View from android.view.View. 146 */ 147 String binaryToInternalClassName(String className) { 148 if (className == null) { 149 return null; 150 } else { 151 return className.replace('.', '/'); 152 } 153 } 154 155 /** Sets the map of classes to output as-is, except if they have native methods */ 156 public void setKeep(Map<String, ClassReader> keep) { 157 mKeep = keep; 158 } 159 160 /** Sets the map of dependencies that must be completely stubbed */ 161 public void setDeps(Map<String, ClassReader> deps) { 162 mDeps = deps; 163 } 164 165 /** Gets the map of classes to output as-is, except if they have native methods */ 166 public Map<String, ClassReader> getKeep() { 167 return mKeep; 168 } 169 170 /** Gets the map of dependencies that must be completely stubbed */ 171 public Map<String, ClassReader> getDeps() { 172 return mDeps; 173 } 174 175 /** Generates the final JAR */ 176 public void generate() throws FileNotFoundException, IOException { 177 TreeMap<String, byte[]> all = new TreeMap<String, byte[]>(); 178 179 for (Class<?> clazz : mInjectClasses) { 180 String name = classToEntryPath(clazz); 181 InputStream is = ClassLoader.getSystemResourceAsStream(name); 182 ClassReader cr = new ClassReader(is); 183 byte[] b = transform(cr, true /* stubNativesOnly */); 184 name = classNameToEntryPath(transformName(cr.getClassName())); 185 all.put(name, b); 186 } 187 188 for (Entry<String, ClassReader> entry : mDeps.entrySet()) { 189 ClassReader cr = entry.getValue(); 190 byte[] b = transform(cr, false /* stubNativesOnly */); 191 String name = classNameToEntryPath(transformName(cr.getClassName())); 192 all.put(name, b); 193 } 194 195 for (Entry<String, ClassReader> entry : mKeep.entrySet()) { 196 ClassReader cr = entry.getValue(); 197 byte[] b = transform(cr, true /* stubNativesOnly */); 198 String name = classNameToEntryPath(transformName(cr.getClassName())); 199 all.put(name, b); 200 } 201 202 mLog.info("# deps classes: %d", mDeps.size()); 203 mLog.info("# keep classes: %d", mKeep.size()); 204 mLog.info("# renamed : %d", mRenameCount); 205 206 createJar(new FileOutputStream(mOsDestJar), all); 207 mLog.info("Created JAR file %s", mOsDestJar); 208 } 209 210 /** 211 * Writes the JAR file. 212 * 213 * @param outStream The file output stream were to write the JAR. 214 * @param all The map of all classes to output. 215 * @throws IOException if an I/O error has occurred 216 */ 217 void createJar(FileOutputStream outStream, Map<String,byte[]> all) throws IOException { 218 JarOutputStream jar = new JarOutputStream(outStream); 219 for (Entry<String, byte[]> entry : all.entrySet()) { 220 String name = entry.getKey(); 221 JarEntry jar_entry = new JarEntry(name); 222 jar.putNextEntry(jar_entry); 223 jar.write(entry.getValue()); 224 jar.closeEntry(); 225 } 226 jar.flush(); 227 jar.close(); 228 } 229 230 /** 231 * Utility method that converts a fully qualified java name into a JAR entry path 232 * e.g. for the input "android.view.View" it returns "android/view/View.class" 233 */ 234 String classNameToEntryPath(String className) { 235 return className.replaceAll("\\.", "/").concat(".class"); 236 } 237 238 /** 239 * Utility method to get the JAR entry path from a Class name. 240 * e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class" 241 */ 242 private String classToEntryPath(Class<?> clazz) { 243 String name = ""; 244 Class<?> parent; 245 while ((parent = clazz.getEnclosingClass()) != null) { 246 name = "$" + clazz.getSimpleName() + name; 247 clazz = parent; 248 } 249 return classNameToEntryPath(clazz.getCanonicalName() + name); 250 } 251 252 /** 253 * Transforms a class. 254 * <p/> 255 * There are 3 kind of transformations: 256 * 257 * 1- For "mock" dependencies classes, we want to remove all code from methods and replace 258 * by a stub. Native methods must be implemented with this stub too. Abstract methods are 259 * left intact. Modified classes must be overridable (non-private, non-final). 260 * Native methods must be made non-final, non-private. 261 * 262 * 2- For "keep" classes, we want to rewrite all native methods as indicated above. 263 * If a class has native methods, it must also be made non-private, non-final. 264 * 265 * Note that unfortunately static methods cannot be changed to non-static (since static and 266 * non-static are invoked differently.) 267 */ 268 byte[] transform(ClassReader cr, boolean stubNativesOnly) { 269 270 boolean hasNativeMethods = hasNativeMethods(cr); 271 String className = cr.getClassName(); 272 273 String newName = transformName(className); 274 // transformName returns its input argument if there's no need to rename the class 275 if (newName != className) { 276 mRenameCount++; 277 // This class is being renamed, so remove it from the list of classes not renamed. 278 mClassesNotRenamed.remove(className); 279 } 280 281 mLog.debug("Transform %s%s%s%s", className, 282 newName == className ? "" : " (renamed to " + newName + ")", 283 hasNativeMethods ? " -- has natives" : "", 284 stubNativesOnly ? " -- stub natives only" : ""); 285 286 // Rewrite the new class from scratch, without reusing the constant pool from the 287 // original class reader. 288 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 289 290 ClassVisitor rv = cw; 291 if (newName != className) { 292 rv = new RenameClassAdapter(cw, className, newName); 293 } 294 295 TransformClassAdapter cv = new TransformClassAdapter(mLog, mStubMethods, 296 mDeleteReturns.get(className), 297 newName, rv, 298 stubNativesOnly, stubNativesOnly || hasNativeMethods); 299 cr.accept(cv, 0 /* flags */); 300 return cw.toByteArray(); 301 } 302 303 /** 304 * Should this class be renamed, this returns the new name. Otherwise it returns the 305 * original name. 306 * 307 * @param className The internal ASM name of the class that may have to be renamed 308 * @return A new transformed name or the original input argument. 309 */ 310 String transformName(String className) { 311 String newName = mRenameClasses.get(className); 312 if (newName != null) { 313 return newName; 314 } 315 int pos = className.indexOf('$'); 316 if (pos > 0) { 317 // Is this an inner class of a renamed class? 318 String base = className.substring(0, pos); 319 newName = mRenameClasses.get(base); 320 if (newName != null) { 321 return newName + className.substring(pos); 322 } 323 } 324 325 return className; 326 } 327 328 /** 329 * Returns true if a class has any native methods. 330 */ 331 boolean hasNativeMethods(ClassReader cr) { 332 ClassHasNativeVisitor cv = new ClassHasNativeVisitor(); 333 cr.accept(cv, 0 /* flags */); 334 return cv.hasNativeMethods(); 335 } 336 337} 338