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