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