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