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