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