AsmGenerator.java revision 8a16f8e29a67c383e065ba9c47d5d1e80d7d2db9
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.ByteArrayOutputStream; 24import java.io.FileNotFoundException; 25import java.io.FileOutputStream; 26import java.io.IOException; 27import java.io.InputStream; 28import java.util.Arrays; 29import java.util.HashMap; 30import java.util.HashSet; 31import java.util.Map; 32import java.util.Map.Entry; 33import java.util.Set; 34import java.util.TreeMap; 35import java.util.jar.JarEntry; 36import java.util.jar.JarOutputStream; 37 38/** 39 * Class that generates a new JAR from a list of classes, some of which are to be kept as-is 40 * and some of which are to be stubbed partially or totally. 41 */ 42public class AsmGenerator { 43 44 /** Output logger. */ 45 private final Log mLog; 46 /** The path of the destination JAR to create. */ 47 private final String mOsDestJar; 48 /** List of classes to inject in the final JAR from _this_ archive. */ 49 private final Class<?>[] mInjectClasses; 50 /** The set of methods to stub out. */ 51 private final Set<String> mStubMethods; 52 /** All classes to output as-is, except if they have native methods. */ 53 private Map<String, ClassReader> mKeep; 54 /** All dependencies that must be completely stubbed. */ 55 private Map<String, ClassReader> mDeps; 56 /** All files that are to be copied as-is. */ 57 private Map<String, InputStream> mCopyFiles; 58 /** Counter of number of classes renamed during transform. */ 59 private int mRenameCount; 60 /** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */ 61 private final HashMap<String, String> mRenameClasses; 62 /** FQCN Names of "old" classes that were NOT renamed. This starts with the full list of 63 * old-FQCN to rename and they get erased as they get renamed. At the end, classes still 64 * left here are not in the code base anymore and thus were not renamed. */ 65 private HashSet<String> mClassesNotRenamed; 66 /** A map { FQCN => set { list of return types to delete from the FQCN } }. */ 67 private HashMap<String, Set<String>> mDeleteReturns; 68 /** A map { FQCN => set { method names } } of methods to rewrite as delegates. 69 * The special name {@link DelegateClassAdapter#ALL_NATIVES} can be used as in internal set. */ 70 private final HashMap<String, Set<String>> mDelegateMethods; 71 /** FQCN Names of classes to refactor. All reference to old-FQCN will be updated to new-FQCN. 72 * map old-FQCN => new-FQCN */ 73 private final HashMap<String, String> mRefactorClasses; 74 75 /** 76 * Creates a new generator that can generate the output JAR with the stubbed classes. 77 * 78 * @param log Output logger. 79 * @param osDestJar The path of the destination JAR to create. 80 * @param createInfo Creation parameters. Must not be null. 81 */ 82 public AsmGenerator(Log log, String osDestJar, ICreateInfo createInfo) { 83 mLog = log; 84 mOsDestJar = osDestJar; 85 mInjectClasses = createInfo.getInjectedClasses(); 86 mStubMethods = new HashSet<String>(Arrays.asList(createInfo.getOverriddenMethods())); 87 88 // Create the map/set of methods to change to delegates 89 mDelegateMethods = new HashMap<String, Set<String>>(); 90 for (String signature : createInfo.getDelegateMethods()) { 91 int pos = signature.indexOf('#'); 92 if (pos <= 0 || pos >= signature.length() - 1) { 93 continue; 94 } 95 String className = binaryToInternalClassName(signature.substring(0, pos)); 96 String methodName = signature.substring(pos + 1); 97 Set<String> methods = mDelegateMethods.get(className); 98 if (methods == null) { 99 methods = new HashSet<String>(); 100 mDelegateMethods.put(className, methods); 101 } 102 methods.add(methodName); 103 } 104 for (String className : createInfo.getDelegateClassNatives()) { 105 className = binaryToInternalClassName(className); 106 Set<String> methods = mDelegateMethods.get(className); 107 if (methods == null) { 108 methods = new HashSet<String>(); 109 mDelegateMethods.put(className, methods); 110 } 111 methods.add(DelegateClassAdapter.ALL_NATIVES); 112 } 113 114 // Create the map of classes to rename. 115 mRenameClasses = new HashMap<String, String>(); 116 mClassesNotRenamed = new HashSet<String>(); 117 String[] renameClasses = createInfo.getRenamedClasses(); 118 int n = renameClasses.length; 119 for (int i = 0; i < n; i += 2) { 120 assert i + 1 < n; 121 // The ASM class names uses "/" separators, whereas regular FQCN use "." 122 String oldFqcn = binaryToInternalClassName(renameClasses[i]); 123 String newFqcn = binaryToInternalClassName(renameClasses[i + 1]); 124 mRenameClasses.put(oldFqcn, newFqcn); 125 mClassesNotRenamed.add(oldFqcn); 126 } 127 128 // Create a map of classes to be refactored. 129 mRefactorClasses = new HashMap<String, String>(); 130 String[] refactorClasses = createInfo.getJavaPkgClasses(); 131 n = refactorClasses.length; 132 for (int i = 0; i < n; i += 2) { 133 assert i + 1 < n; 134 String oldFqcn = binaryToInternalClassName(refactorClasses[i]); 135 String newFqcn = binaryToInternalClassName(refactorClasses[i + 1]); 136 mRefactorClasses.put(oldFqcn, newFqcn);; 137 } 138 139 // create the map of renamed class -> return type of method to delete. 140 mDeleteReturns = new HashMap<String, Set<String>>(); 141 String[] deleteReturns = createInfo.getDeleteReturns(); 142 Set<String> returnTypes = null; 143 String renamedClass = null; 144 for (String className : deleteReturns) { 145 // if we reach the end of a section, add it to the main map 146 if (className == null) { 147 if (returnTypes != null) { 148 mDeleteReturns.put(renamedClass, returnTypes); 149 } 150 151 renamedClass = null; 152 continue; 153 } 154 155 // if the renamed class is null, this is the beginning of a section 156 if (renamedClass == null) { 157 renamedClass = binaryToInternalClassName(className); 158 continue; 159 } 160 161 // just a standard return type, we add it to the list. 162 if (returnTypes == null) { 163 returnTypes = new HashSet<String>(); 164 } 165 returnTypes.add(binaryToInternalClassName(className)); 166 } 167 } 168 169 /** 170 * Returns the list of classes that have not been renamed yet. 171 * <p/> 172 * The names are "internal class names" rather than FQCN, i.e. they use "/" instead "." 173 * as package separators. 174 */ 175 public Set<String> getClassesNotRenamed() { 176 return mClassesNotRenamed; 177 } 178 179 /** 180 * Utility that returns the internal ASM class name from a fully qualified binary class 181 * name. E.g. it returns android/view/View from android.view.View. 182 */ 183 String binaryToInternalClassName(String className) { 184 if (className == null) { 185 return null; 186 } else { 187 return className.replace('.', '/'); 188 } 189 } 190 191 /** Sets the map of classes to output as-is, except if they have native methods */ 192 public void setKeep(Map<String, ClassReader> keep) { 193 mKeep = keep; 194 } 195 196 /** Sets the map of dependencies that must be completely stubbed */ 197 public void setDeps(Map<String, ClassReader> deps) { 198 mDeps = deps; 199 } 200 201 /** Sets the map of files to output as-is. */ 202 public void setCopyFiles(Map<String, InputStream> copyFiles) { 203 mCopyFiles = copyFiles; 204 } 205 206 /** Gets the map of classes to output as-is, except if they have native methods */ 207 public Map<String, ClassReader> getKeep() { 208 return mKeep; 209 } 210 211 /** Gets the map of dependencies that must be completely stubbed */ 212 public Map<String, ClassReader> getDeps() { 213 return mDeps; 214 } 215 216 /** Gets the map of files to output as-is. */ 217 public Map<String, InputStream> getCopyFiles() { 218 return mCopyFiles; 219 } 220 221 /** Generates the final JAR */ 222 public void generate() throws FileNotFoundException, IOException { 223 TreeMap<String, byte[]> all = new TreeMap<String, byte[]>(); 224 225 for (Class<?> clazz : mInjectClasses) { 226 String name = classToEntryPath(clazz); 227 InputStream is = ClassLoader.getSystemResourceAsStream(name); 228 ClassReader cr = new ClassReader(is); 229 byte[] b = transform(cr, true /* stubNativesOnly */); 230 name = classNameToEntryPath(transformName(cr.getClassName())); 231 all.put(name, b); 232 } 233 234 for (Entry<String, ClassReader> entry : mDeps.entrySet()) { 235 ClassReader cr = entry.getValue(); 236 byte[] b = transform(cr, true /* stubNativesOnly */); 237 String name = classNameToEntryPath(transformName(cr.getClassName())); 238 all.put(name, b); 239 } 240 241 for (Entry<String, ClassReader> entry : mKeep.entrySet()) { 242 ClassReader cr = entry.getValue(); 243 byte[] b = transform(cr, true /* stubNativesOnly */); 244 String name = classNameToEntryPath(transformName(cr.getClassName())); 245 all.put(name, b); 246 } 247 248 for (Entry<String, InputStream> entry : mCopyFiles.entrySet()) { 249 try { 250 byte[] b = inputStreamToByteArray(entry.getValue()); 251 all.put(entry.getKey(), b); 252 } catch (IOException e) { 253 // Ignore. 254 } 255 256 } 257 mLog.info("# deps classes: %d", mDeps.size()); 258 mLog.info("# keep classes: %d", mKeep.size()); 259 mLog.info("# renamed : %d", mRenameCount); 260 261 createJar(new FileOutputStream(mOsDestJar), all); 262 mLog.info("Created JAR file %s", mOsDestJar); 263 } 264 265 /** 266 * Writes the JAR file. 267 * 268 * @param outStream The file output stream were to write the JAR. 269 * @param all The map of all classes to output. 270 * @throws IOException if an I/O error has occurred 271 */ 272 void createJar(FileOutputStream outStream, Map<String,byte[]> all) throws IOException { 273 JarOutputStream jar = new JarOutputStream(outStream); 274 for (Entry<String, byte[]> entry : all.entrySet()) { 275 String name = entry.getKey(); 276 JarEntry jar_entry = new JarEntry(name); 277 jar.putNextEntry(jar_entry); 278 jar.write(entry.getValue()); 279 jar.closeEntry(); 280 } 281 jar.flush(); 282 jar.close(); 283 } 284 285 /** 286 * Utility method that converts a fully qualified java name into a JAR entry path 287 * e.g. for the input "android.view.View" it returns "android/view/View.class" 288 */ 289 String classNameToEntryPath(String className) { 290 return className.replaceAll("\\.", "/").concat(".class"); 291 } 292 293 /** 294 * Utility method to get the JAR entry path from a Class name. 295 * e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class" 296 */ 297 private String classToEntryPath(Class<?> clazz) { 298 String name = ""; 299 Class<?> parent; 300 while ((parent = clazz.getEnclosingClass()) != null) { 301 name = "$" + clazz.getSimpleName() + name; 302 clazz = parent; 303 } 304 return classNameToEntryPath(clazz.getCanonicalName() + name); 305 } 306 307 /** 308 * Transforms a class. 309 * <p/> 310 * There are 3 kind of transformations: 311 * 312 * 1- For "mock" dependencies classes, we want to remove all code from methods and replace 313 * by a stub. Native methods must be implemented with this stub too. Abstract methods are 314 * left intact. Modified classes must be overridable (non-private, non-final). 315 * Native methods must be made non-final, non-private. 316 * 317 * 2- For "keep" classes, we want to rewrite all native methods as indicated above. 318 * If a class has native methods, it must also be made non-private, non-final. 319 * 320 * Note that unfortunately static methods cannot be changed to non-static (since static and 321 * non-static are invoked differently.) 322 */ 323 byte[] transform(ClassReader cr, boolean stubNativesOnly) { 324 325 boolean hasNativeMethods = hasNativeMethods(cr); 326 327 // Get the class name, as an internal name (e.g. com/android/SomeClass$InnerClass) 328 String className = cr.getClassName(); 329 330 String newName = transformName(className); 331 // transformName returns its input argument if there's no need to rename the class 332 if (newName != className) { 333 mRenameCount++; 334 // This class is being renamed, so remove it from the list of classes not renamed. 335 mClassesNotRenamed.remove(className); 336 } 337 338 mLog.debug("Transform %s%s%s%s", className, 339 newName == className ? "" : " (renamed to " + newName + ")", 340 hasNativeMethods ? " -- has natives" : "", 341 stubNativesOnly ? " -- stub natives only" : ""); 342 343 // Rewrite the new class from scratch, without reusing the constant pool from the 344 // original class reader. 345 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 346 347 ClassVisitor cv = new RefactorClassAdapter(cw, mRefactorClasses); 348 if (newName != className) { 349 cv = new RenameClassAdapter(cv, className, newName); 350 } 351 352 cv = new TransformClassAdapter(mLog, mStubMethods, 353 mDeleteReturns.get(className), 354 newName, cv, 355 stubNativesOnly, stubNativesOnly || hasNativeMethods); 356 357 Set<String> delegateMethods = mDelegateMethods.get(className); 358 if (delegateMethods != null && !delegateMethods.isEmpty()) { 359 // If delegateMethods only contains one entry ALL_NATIVES and the class is 360 // known to have no native methods, just skip this step. 361 if (hasNativeMethods || 362 !(delegateMethods.size() == 1 && 363 delegateMethods.contains(DelegateClassAdapter.ALL_NATIVES))) { 364 cv = new DelegateClassAdapter(mLog, cv, className, delegateMethods); 365 } 366 } 367 368 cr.accept(cv, 0 /* flags */); 369 return cw.toByteArray(); 370 } 371 372 /** 373 * Should this class be renamed, this returns the new name. Otherwise it returns the 374 * original name. 375 * 376 * @param className The internal ASM name of the class that may have to be renamed 377 * @return A new transformed name or the original input argument. 378 */ 379 String transformName(String className) { 380 String newName = mRenameClasses.get(className); 381 if (newName != null) { 382 return newName; 383 } 384 int pos = className.indexOf('$'); 385 if (pos > 0) { 386 // Is this an inner class of a renamed class? 387 String base = className.substring(0, pos); 388 newName = mRenameClasses.get(base); 389 if (newName != null) { 390 return newName + className.substring(pos); 391 } 392 } 393 394 return className; 395 } 396 397 /** 398 * Returns true if a class has any native methods. 399 */ 400 boolean hasNativeMethods(ClassReader cr) { 401 ClassHasNativeVisitor cv = new ClassHasNativeVisitor(); 402 cr.accept(cv, 0 /* flags */); 403 return cv.hasNativeMethods(); 404 } 405 406 private byte[] inputStreamToByteArray(InputStream is) throws IOException { 407 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 408 byte[] data = new byte[8192]; // 8KB 409 int n; 410 while ((n = is.read(data, 0, data.length)) != -1) { 411 buffer.write(data, 0, n); 412 } 413 return buffer.toByteArray(); 414 } 415} 416