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