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