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