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