1 /*
2  * Copyright (C) 2009 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 
17 package com.android.mkstubs;
18 
19 import org.objectweb.asm.ClassReader;
20 import org.objectweb.asm.Opcodes;
21 
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.util.Map;
27 
28 
29 /**
30  * Main entry point of the MkStubs app.
31  * <p/>
32  * For workflow details, see {@link #process(Params)}.
33  */
34 public class Main {
35 
36     public static final int ASM_VERSION = Opcodes.ASM5;
37 
38     /**
39      * A struct-like class to hold the various input values (e.g. command-line args)
40      */
41     static class Params {
42         private String mInputJarPath;
43         private String mOutputJarPath;
44         private Filter mFilter;
45         private boolean mVerbose;
46         private boolean mDumpSource;
47 
Params()48         public Params() {
49             mFilter = new Filter();
50         }
51 
52         /** Sets the name of the input jar, where to read classes from. Must not be null. */
setInputJarPath(String inputJarPath)53         public void setInputJarPath(String inputJarPath) {
54             mInputJarPath = inputJarPath;
55         }
56 
57         /** Sets the name of the output jar, where to write classes to. Must not be null. */
setOutputJarPath(String outputJarPath)58         public void setOutputJarPath(String outputJarPath) {
59             mOutputJarPath = outputJarPath;
60         }
61 
62         /** Returns the name of the input jar, where to read classes from. */
getInputJarPath()63         public String getInputJarPath() {
64             return mInputJarPath;
65         }
66 
67         /** Returns the name of the output jar, where to write classes to. */
getOutputJarPath()68         public String getOutputJarPath() {
69             return mOutputJarPath;
70         }
71 
72         /** Returns the current instance of the filter, the include/exclude patterns. */
getFilter()73         public Filter getFilter() {
74             return mFilter;
75         }
76 
77         /** Sets verbose mode on. Default is off. */
setVerbose()78         public void setVerbose() {
79             mVerbose = true;
80         }
81 
82         /** Returns true if verbose mode is on. */
isVerbose()83         public boolean isVerbose() {
84             return mVerbose;
85         }
86 
87         /** Sets dump source mode on. Default is off. */
setDumpSource()88         public void setDumpSource() {
89             mDumpSource = true;
90         }
91 
92         /** Returns true if source should be dumped. */
isDumpSource()93         public boolean isDumpSource() {
94             return mDumpSource;
95         }
96     }
97 
98     /** Logger that writes on stdout depending a conditional verbose mode. */
99     static class Logger {
100         private final boolean mVerbose;
101 
Logger(boolean verbose)102         public Logger(boolean verbose) {
103             mVerbose = verbose;
104         }
105 
106         /** Writes to stdout only in verbose mode. */
debug(String msg, Object...params)107         public void debug(String msg, Object...params) {
108             if (mVerbose) {
109                 System.out.println(String.format(msg, params));
110             }
111         }
112 
113         /** Writes to stdout all the time. */
info(String msg, Object...params)114         public void info(String msg, Object...params) {
115             System.out.println(String.format(msg, params));
116         }
117     }
118 
119     /**
120      * Main entry point. Processes arguments then performs the "real" work.
121      */
main(String[] args)122     public static void main(String[] args) {
123         Main m = new Main();
124         try {
125             Params p = m.processArgs(args);
126             m.process(p);
127         } catch (IOException e) {
128             e.printStackTrace();
129         }
130     }
131 
132     /**
133      * Grabs command-line arguments.
134      * The expected arguments are:
135      * <ul>
136      * <li> The filename of the input Jar.
137      * <li> The filename of the output Jar.
138      * <li> One or more include/exclude patterns or files containing these patterns.
139      *      See {@link #addString(Params, String)} for syntax.
140      * </ul>
141      * @throws IOException on failure to read a pattern file.
142      */
processArgs(String[] args)143     private Params processArgs(String[] args) throws IOException {
144         Params p = new Params();
145 
146         for (String arg : args) {
147             if (arg.startsWith("--")) {
148                 if (arg.startsWith("--v")) {
149                     p.setVerbose();
150                 } else if (arg.startsWith("--s")) {
151                     p.setDumpSource();
152                 } else if (arg.startsWith("--h")) {
153                     usage(null);
154                 } else {
155                     usage("Unknown argument: " + arg);
156                 }
157             } else if (p.getInputJarPath() == null) {
158                 p.setInputJarPath(arg);
159             } else if (p.getOutputJarPath() == null) {
160                 p.setOutputJarPath(arg);
161             } else {
162                 addString(p, arg);
163             }
164         }
165 
166         if (p.getInputJarPath() == null && p.getOutputJarPath() == null) {
167             usage("Missing input or output JAR.");
168         }
169 
170         return p;
171     }
172 
173     /**
174      * Adds one pattern string to the current filter.
175      * The syntax must be:
176      * <ul>
177      * <li> +full_include or +prefix_include*
178      * <li> -full_exclude or -prefix_exclude*
179      * <li> @filename
180      * </ul>
181      * The input string is trimmed so any space around the first letter (-/+/@) or
182      * at the end is removed. Empty strings are ignored.
183      *
184      * @param p The params which filters to edit.
185      * @param s The string to examine.
186      * @throws IOException
187      */
addString(Params p, String s)188     private void addString(Params p, String s) throws IOException {
189         if (s == null) {
190             return;
191         }
192 
193         s = s.trim();
194 
195         if (s.length() < 2) {
196             return;
197         }
198 
199         char mode = s.charAt(0);
200         s = s.substring(1).trim();
201 
202         if (mode == '@') {
203             addStringsFromFile(p, s);
204 
205         } else if (mode == '-') {
206             s = s.replace('.', '/');  // transform FQCN into ASM internal name
207             if (s.endsWith("*")) {
208                 p.getFilter().getExcludePrefix().add(s.substring(0, s.length() - 1));
209             } else {
210                 p.getFilter().getExcludeFull().add(s);
211             }
212 
213         } else if (mode == '+') {
214             s = s.replace('.', '/');  // transform FQCN into ASM internal name
215             if (s.endsWith("*")) {
216                 p.getFilter().getIncludePrefix().add(s.substring(0, s.length() - 1));
217             } else {
218                 p.getFilter().getIncludeFull().add(s);
219             }
220         }
221     }
222 
223     /**
224      * Adds all the filter strings from the given file.
225      *
226      * @param p The params which filter to edit.
227      * @param osFilePath The OS path to the file containing the patterns.
228      * @throws IOException
229      *
230      * @see #addString(Params, String)
231      */
addStringsFromFile(Params p, String osFilePath)232     private void addStringsFromFile(Params p, String osFilePath)
233             throws IOException {
234         BufferedReader br = null;
235         try {
236             br = new BufferedReader(new FileReader(osFilePath));
237             String line;
238             while ((line = br.readLine()) != null) {
239                 addString(p, line);
240             }
241         } finally {
242             if (br != null) {
243                 br.close();
244             }
245         }
246     }
247 
248     /**
249      * Prints some help to stdout.
250      * @param error The error that generated the usage, if any. Can be null.
251      */
usage(String error)252     private void usage(String error) {
253         if (error != null) {
254             System.out.println("ERROR: " + error);
255         }
256 
257         System.out.println("Usage: mkstub [--h|--s|--v] input.jar output.jar [excluded-class @excluded-classes-file ...]");
258 
259         System.out.println("Options:\n" +
260                 " --h | --help    : print this usage.\n" +
261                 " --v | --verbose : verbose mode.\n" +
262                 " --s | --source  : dump source equivalent to modified byte code.\n\n");
263 
264         System.out.println("Include syntax:\n" +
265                 "+com.package.* : whole package, with glob\n" +
266                 "+com.package.Class[$Inner] or ...Class*: whole classes with optional glob\n" +
267                 "Inclusion is not supported at method/field level.\n\n");
268 
269         System.out.println("Exclude syntax:\n" +
270         		"-com.package.* : whole package, with glob\n" +
271         		"-com.package.Class[$Inner] or ...Class*: whole classes with optional glob\n" +
272         		"-com.package.Class#method: whole method or field\n" +
273                 "-com.package.Class#method(IILjava/lang/String;)V: specific method with signature.\n\n");
274 
275         System.exit(1);
276     }
277 
278     /**
279      * Performs the main workflow of this app:
280      * <ul>
281      * <li> Read the input Jar to get all its classes.
282      * <li> Filter out all classes that should not be included or that should be excluded.
283      * <li> Goes thru the classes, filters methods/fields and generate their source
284      *      in a directory called "&lt;outpath_jar_path&gt;_sources"
285      * <li> Does the same filtering on the classes but this time generates the real stubbed
286      *      output jar.
287      * </ul>
288      */
process(Params p)289     private void process(Params p) throws IOException {
290         AsmAnalyzer aa = new AsmAnalyzer();
291         Map<String, ClassReader> classes = aa.parseInputJar(p.getInputJarPath());
292 
293         Logger log = new Logger(p.isVerbose());
294         log.info("Classes loaded: %d", classes.size());
295 
296         aa.filter(classes, p.getFilter(), log);
297         log.info("Classes filtered: %d", classes.size());
298 
299         // dump as Java source files, mostly for debugging
300         if (p.isDumpSource()) {
301             SourceGenerator src_gen = new SourceGenerator(log);
302             File dst_src_dir = new File(p.getOutputJarPath() + "_sources");
303             dst_src_dir.mkdir();
304             src_gen.generateSource(dst_src_dir, classes, p.getFilter());
305         }
306 
307         // dump the stubbed jar
308         StubGenerator stub_gen = new StubGenerator(log);
309         File dst_jar = new File(p.getOutputJarPath());
310         stub_gen.generateStubbedJar(dst_jar, classes, p.getFilter());
311     }
312 }
313