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