/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mkstubs; import com.android.mkstubs.Main.Params; import org.objectweb.asm.ClassReader; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.Map; /** * Main entry point of the MkStubs app. * <p/> * For workflow details, see {@link #process(Params)}. */ public class Main { /** * A struct-like class to hold the various input values (e.g. command-line args) */ static class Params { private String mInputJarPath; private String mOutputJarPath; private Filter mFilter; private boolean mVerbose; private boolean mDumpSource; public Params() { mFilter = new Filter(); } /** Sets the name of the input jar, where to read classes from. Must not be null. */ public void setInputJarPath(String inputJarPath) { mInputJarPath = inputJarPath; } /** Sets the name of the output jar, where to write classes to. Must not be null. */ public void setOutputJarPath(String outputJarPath) { mOutputJarPath = outputJarPath; } /** Returns the name of the input jar, where to read classes from. */ public String getInputJarPath() { return mInputJarPath; } /** Returns the name of the output jar, where to write classes to. */ public String getOutputJarPath() { return mOutputJarPath; } /** Returns the current instance of the filter, the include/exclude patterns. */ public Filter getFilter() { return mFilter; } /** Sets verbose mode on. Default is off. */ public void setVerbose() { mVerbose = true; } /** Returns true if verbose mode is on. */ public boolean isVerbose() { return mVerbose; } /** Sets dump source mode on. Default is off. */ public void setDumpSource() { mDumpSource = true; } /** Returns true if source should be dumped. */ public boolean isDumpSource() { return mDumpSource; } } /** Logger that writes on stdout depending a conditional verbose mode. */ static class Logger { private final boolean mVerbose; public Logger(boolean verbose) { mVerbose = verbose; } /** Writes to stdout only in verbose mode. */ public void debug(String msg, Object...params) { if (mVerbose) { System.out.println(String.format(msg, params)); } } /** Writes to stdout all the time. */ public void info(String msg, Object...params) { System.out.println(String.format(msg, params)); } } /** * Main entry point. Processes arguments then performs the "real" work. */ public static void main(String[] args) { Main m = new Main(); try { Params p = m.processArgs(args); m.process(p); } catch (IOException e) { e.printStackTrace(); } } /** * Grabs command-line arguments. * The expected arguments are: * <ul> * <li> The filename of the input Jar. * <li> The filename of the output Jar. * <li> One or more include/exclude patterns or files containing these patterns. * See {@link #addString(Params, String)} for syntax. * </ul> * @throws IOException on failure to read a pattern file. */ private Params processArgs(String[] args) throws IOException { Params p = new Params(); for (String arg : args) { if (arg.startsWith("--")) { if (arg.startsWith("--v")) { p.setVerbose(); } else if (arg.startsWith("--s")) { p.setDumpSource(); } else if (arg.startsWith("--h")) { usage(null); } else { usage("Unknown argument: " + arg); } } else if (p.getInputJarPath() == null) { p.setInputJarPath(arg); } else if (p.getOutputJarPath() == null) { p.setOutputJarPath(arg); } else { addString(p, arg); } } if (p.getInputJarPath() == null && p.getOutputJarPath() == null) { usage("Missing input or output JAR."); } return p; } /** * Adds one pattern string to the current filter. * The syntax must be: * <ul> * <li> +full_include or +prefix_include* * <li> -full_exclude or -prefix_exclude* * <li> @filename * </ul> * The input string is trimmed so any space around the first letter (-/+/@) or * at the end is removed. Empty strings are ignored. * * @param p The params which filters to edit. * @param s The string to examine. * @throws IOException */ private void addString(Params p, String s) throws IOException { if (s == null) { return; } s = s.trim(); if (s.length() < 2) { return; } char mode = s.charAt(0); s = s.substring(1).trim(); if (mode == '@') { addStringsFromFile(p, s); } else if (mode == '-') { s = s.replace('.', '/'); // transform FQCN into ASM internal name if (s.endsWith("*")) { p.getFilter().getExcludePrefix().add(s.substring(0, s.length() - 1)); } else { p.getFilter().getExcludeFull().add(s); } } else if (mode == '+') { s = s.replace('.', '/'); // transform FQCN into ASM internal name if (s.endsWith("*")) { p.getFilter().getIncludePrefix().add(s.substring(0, s.length() - 1)); } else { p.getFilter().getIncludeFull().add(s); } } } /** * Adds all the filter strings from the given file. * * @param p The params which filter to edit. * @param osFilePath The OS path to the file containing the patterns. * @throws IOException * * @see #addString(Params, String) */ private void addStringsFromFile(Params p, String osFilePath) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(osFilePath)); String line; while ((line = br.readLine()) != null) { addString(p, line); } } finally { if (br != null) { br.close(); } } } /** * Prints some help to stdout. * @param error The error that generated the usage, if any. Can be null. */ private void usage(String error) { if (error != null) { System.out.println("ERROR: " + error); } System.out.println("Usage: mkstub [--h|--s|--v] input.jar output.jar [excluded-class @excluded-classes-file ...]"); System.out.println("Options:\n" + " --h | --help : print this usage.\n" + " --v | --verbose : verbose mode.\n" + " --s | --source : dump source equivalent to modified byte code.\n\n"); System.out.println("Include syntax:\n" + "+com.package.* : whole package, with glob\n" + "+com.package.Class[$Inner] or ...Class*: whole classes with optional glob\n" + "Inclusion is not supported at method/field level.\n\n"); System.out.println("Exclude syntax:\n" + "-com.package.* : whole package, with glob\n" + "-com.package.Class[$Inner] or ...Class*: whole classes with optional glob\n" + "-com.package.Class#method: whole method or field\n" + "-com.package.Class#method(IILjava/lang/String;)V: specific method with signature.\n\n"); System.exit(1); } /** * Performs the main workflow of this app: * <ul> * <li> Read the input Jar to get all its classes. * <li> Filter out all classes that should not be included or that should be excluded. * <li> Goes thru the classes, filters methods/fields and generate their source * in a directory called "<outpath_jar_path>_sources" * <li> Does the same filtering on the classes but this time generates the real stubbed * output jar. * </ul> */ private void process(Params p) throws IOException { AsmAnalyzer aa = new AsmAnalyzer(); Map<String, ClassReader> classes = aa.parseInputJar(p.getInputJarPath()); Logger log = new Logger(p.isVerbose()); log.info("Classes loaded: %d", classes.size()); aa.filter(classes, p.getFilter(), log); log.info("Classes filtered: %d", classes.size()); // dump as Java source files, mostly for debugging if (p.isDumpSource()) { SourceGenerator src_gen = new SourceGenerator(log); File dst_src_dir = new File(p.getOutputJarPath() + "_sources"); dst_src_dir.mkdir(); src_gen.generateSource(dst_src_dir, classes, p.getFilter()); } // dump the stubbed jar StubGenerator stub_gen = new StubGenerator(log); File dst_jar = new File(p.getOutputJarPath()); stub_gen.generateStubbedJar(dst_jar, classes, p.getFilter()); } }