blob: 528d6c274e1bb55f279338b9f33d5e9cd912c415 [file] [log] [blame]
/*
* Copyright (C) 2022 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 libcore.tools.analyzer.openjdk;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import libcore.tools.analyzer.openjdk.DependencyAnalyzer.Result.MethodDependency;
import libcore.tools.analyzer.openjdk.SignaturesCollector.SignaturesCollection;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.TraceClassVisitor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Main {
private static class MainArgs {
@Parameter(names = "-h", help = true, description = "Shows this help message")
public boolean help = false;
}
@Parameters(commandNames = CommandDump.NAME, commandDescription = "Dump a class from the "
+ "classpath. This tool is similar to javap tool provided in the OpenJDK.")
private static class CommandDump {
public static final String NAME = "dump";
@Parameter(names = {"-cp", "--classpath"}, description = "file path to a .jmod or .jar file"
+ " or one of the following java version: oj, 8, 9, 11, 17")
public String classpathFile = "17";
@Parameter(required = true, arity = 1,
description = "<class>. The fully qualified name of the class in the classpath. "
+ "Note that inner class uses $ separator and $ has to be escaped in shell,"
+ " e.g. java.lang.Character\\$Subset")
public List<String> classNames;
@Parameter(names = "-h", help = true, description = "Shows this help message")
public boolean help = false;
private void run() throws UncheckedIOException {
Path jmod = AndroidHostEnvUtil.parseInputClasspath(classpathFile);
String className = classNames.get(0);
try (ZipFile zipFile = new ZipFile(jmod.toFile())) {
ZipEntry e = ClassFileUtil.getEntryFromClassNameOrThrow(zipFile, className);
try (InputStream in = zipFile.getInputStream(e)) {
ClassReader classReader = new ClassReader(in);
PrintWriter printer = new PrintWriter(System.out);
TraceClassVisitor traceClassVisitor = new TraceClassVisitor(printer);
classReader.accept(traceClassVisitor, 0);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
@Parameters(commandNames = CommandApiDiff.NAME, commandDescription = "List the new and "
+ "removed APIs by comparing the older and newer implementation in 2 jar/jmod files")
private static class CommandApiDiff {
public static final String NAME = "api-diff";
@Parameter(names = {"-b", "--base"}, description = "file path to a .jmod or .jar file or "
+ "one of the following java version: oj, 8, 9, 11, 17")
public String baseClasspath = "11";
@Parameter(names = {"-n", "--new"}, description = "file path to a .jmod or .jar file or "
+ "one of the following java version: oj, 8, 9, 11, 17")
public String newClasspath = "17";
@Parameter(required = true, arity = 1,
description = "<class>. The fully qualified name of the class in the classpath. "
+ "Note that inner class uses $ separator and $ has to be escaped in shell,"
+ " e.g. java.lang.Character\\$Subset")
public List<String> classNames;
@Parameter(names = "-h", help = true, description = "Shows this help message")
public boolean help = false;
private void run() throws UncheckedIOException {
Path basePath = AndroidHostEnvUtil.parseInputClasspath(baseClasspath);
Path newPath = AndroidHostEnvUtil.parseInputClasspath(newClasspath);
String className = classNames.get(0);
DiffAnalyzer analyzer;
try (ZipFile baseZip = new ZipFile(basePath.toFile());
ZipFile newZip = new ZipFile(newPath.toFile())) {
ZipEntry baseEntry = ClassFileUtil.getEntryFromClassNameOrThrow(baseZip, className);
ZipEntry newEntry = ClassFileUtil.getEntryFromClassNameOrThrow(newZip, className);
try (InputStream baseIn = baseZip.getInputStream(baseEntry);
InputStream newIn = newZip.getInputStream(newEntry)) {
analyzer = DiffAnalyzer.analyze(baseIn, newIn);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
System.out.println("Class:" + className);
analyzer.print(System.out);
}
}
private static class DiffAnalyzer {
List<MethodNode> newMethods = new ArrayList<>();
List<MethodNode> removedMethods = new ArrayList<>();
List<MethodNode> newlyDeprecatedMethods = new ArrayList<>();
List<FieldNode> newFields = new ArrayList<>();
List<FieldNode> removedFields = new ArrayList<>();
List<FieldNode> newlyDeprecatedFields = new ArrayList<>();
private static DiffAnalyzer analyze(InputStream baseIn, InputStream newIn)
throws IOException {
ClassNode baseClass = ClassFileUtil.parseClass(baseIn);
ClassNode newClass = ClassFileUtil.parseClass(newIn);
return analyze(baseClass, newClass);
}
private static DiffAnalyzer analyze(ClassNode baseClass, ClassNode newClass) {
Map<String, MethodNode> baseMethods = getExposedMethods(baseClass)
.collect(toMap(DiffAnalyzer::toApiSignature, node -> node));
Map<String, MethodNode> newMethods = getExposedMethods(newClass)
.collect(toMap(DiffAnalyzer::toApiSignature, node -> node));
DiffAnalyzer result = new DiffAnalyzer();
result.newMethods = getExposedMethods(newClass)
.filter(node -> !baseMethods.containsKey(toApiSignature(node)))
.collect(toList());
result.removedMethods = getExposedMethods(baseClass)
.filter(node -> !newMethods.containsKey(toApiSignature(node)))
.collect(toList());
result.newlyDeprecatedMethods = getExposedMethods(newClass)
.filter(DiffAnalyzer::isDeprecated)
.filter(node -> !baseMethods.containsKey(toApiSignature(node))
|| !isDeprecated(baseMethods.get(toApiSignature(node))) )
.collect(toList());
Map<String, FieldNode> baseFields = getExposedFields(baseClass)
.collect(toMap(node -> node.name, node -> node));
Map<String, FieldNode> newFields = getExposedFields(newClass)
.collect(toMap(node -> node.name, node -> node));
result.newFields = getExposedFields(newClass)
.filter(node -> !baseFields.containsKey(node.name))
.collect(toList());
result.removedFields = getExposedFields(baseClass)
.filter(node -> !newFields.containsKey(node.name))
.collect(toList());
result.newlyDeprecatedFields = getExposedFields(newClass)
.filter(DiffAnalyzer::isDeprecated)
.filter(node -> !baseFields.containsKey(node.name)
|| !isDeprecated(baseFields.get(node.name)) )
.collect(toList());
return result;
}
/**
* Known issue: this signature doesn't differentiate static and virtual methods.
*/
private static String toApiSignature(MethodNode node) {
return node.name + node.desc;
}
private static Stream<MethodNode> getExposedMethods(ClassNode classNode) {
if (!isExposed(classNode.access)) {
return Stream.empty();
}
return classNode.methods.stream()
.filter(m -> isExposed(m.access))
.flatMap(DiffAnalyzer::getImpliedMethods);
}
/**
* Get all implied methods that are not generated by javac.
*
* For example, {@link dalvik.annotation.codegen.CovariantReturnType} is used by dexers
* to generate synthetic methods with a different return type.
*/
private static Stream<MethodNode> getImpliedMethods(MethodNode node) {
if (node.invisibleAnnotations == null
// Synthetic methods generated by javac can be annotated with
// @CovariantReturnType, but can be safely ignored to avoid double counting.
|| (node.access & Opcodes.ACC_SYNTHETIC) != 0) {
return Stream.of(node);
}
Stream<MethodNode> syntheticMethods = node.invisibleAnnotations.stream()
.flatMap(a -> { // flatten CovariantReturnTypes.value
if (!"Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;"
.equals(a.desc)) {
return Stream.of(a);
}
for (int i = 0; i < a.values.size() - 1; i++) {
if ("value".equals(a.values.get(i))) {
Object type = a.values.get(i + 1);
if (type instanceof List nodes) {
return Stream.concat(Stream.of(a),
(Stream<AnnotationNode>) nodes.stream());
}
}
}
return Stream.of(a);
})
.filter(a -> "Ldalvik/annotation/codegen/CovariantReturnType;".equals(a.desc))
.map(a -> {
for (int i = 0; i < a.values.size() - 1; i++) {
if ("returnType".equals(a.values.get(i))) {
Object type = a.values.get(i + 1);
if (type instanceof Type) {
return (Type) type;
}
}
}
return null;
})
.filter(Predicate.not(Objects::isNull))
.map(returnType -> {
String desc = Type.getMethodDescriptor(returnType,
Type.getArgumentTypes(node.desc));
// It doesn't copy everything, e.g. annotations, but we only need
// access, name and desc for matching purpose.
return new MethodNode(node.access, node.name, desc, null,
node.exceptions.toArray(new String[0]));
});
return Stream.concat(Stream.of(node), syntheticMethods);
}
private static Stream<FieldNode> getExposedFields(ClassNode classNode) {
if (!isExposed(classNode.access)) {
return Stream.empty();
}
return classNode.fields.stream()
.filter(m -> isExposed(m.access));
}
private static boolean isExposed(int flags) {
return (flags & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0;
}
private static boolean isDeprecated(MethodNode node) {
return node.visibleAnnotations != null &&
node.visibleAnnotations.stream()
.anyMatch(anno ->"Ljava/lang/Deprecated;".equals(anno.desc));
}
private static boolean isDeprecated(FieldNode node) {
return node.visibleAnnotations != null &&
node.visibleAnnotations.stream()
.anyMatch(anno ->"Ljava/lang/Deprecated;".equals(anno.desc));
}
void print(OutputStream out) {
PrintWriter writer = new PrintWriter(out, /*autoFlush=*/true);
printMethods(writer, "New methods", newMethods);
printMethods(writer, "Removed methods", removedMethods);
printMethods(writer, "Newly deprecated methods", newlyDeprecatedMethods);
printFields(writer, "New fields", newFields);
printFields(writer, "Removed fields", removedFields);
printFields(writer, "Newly deprecated fields", newlyDeprecatedFields);
}
private static void printMethods(PrintWriter w, String header, List<MethodNode> nodes) {
if (nodes.isEmpty()) {
return;
}
w.println(" " + header + ":");
for (MethodNode node : nodes) {
w.println(" " + toApiSignature(node));
}
}
private static void printFields(PrintWriter w, String header, List<FieldNode> nodes) {
if (nodes.isEmpty()) {
return;
}
w.println(" " + header + ":");
for (FieldNode node : nodes) {
w.println(" " + node.name + ": " + node.desc);
}
}
}
@Parameters(commandNames = CommandShowDeps.NAME, commandDescription = "Show the dependencies of"
+ " classes or packages.")
private static class CommandShowDeps {
public static final String NAME = "show-deps";
@Parameter(names = {"-cp"}, description = "file path to a .jmod or .jar file or "
+ "one of the following java version: oj, 8, 9, 11, 17")
public String classpath = "17";
/**
* @see DependencyAnalyzer.ExcludeClasspathFilter
*/
@Parameter(names = {"-x", "--exclude-deps-in-classpath"}, description = "a file path to "
+ "a .jmod or .jar file or one of the following java version: oj, 8, 9, 11, 17. "
+ "The classes, methods and fields that exist in this classpath are "
+ "excluded from the output list of dependencies.")
public String excludeClasspath = "oj";
@Parameter(required = true,
description = "(<classes>|<packages>). The class or package names in the classpath. "
+ "Note that inner class uses $ separator and $ has to be escaped in shell,"
+ " e.g. java.lang.Character\\$Subset")
public List<String> classesOrPackages;
@Parameter(names = "-i", description = "Show the dependencies within the provided "
+ "<classes> / <packages>")
public boolean includeInternal = false;
/**
* @see DependencyAnalyzer.ExpectedUpstreamFilter
*/
@Parameter(names = "-e", description = "Exclude the existing dependencies in the OpenJDK "
+ "version of the file specified in the libcore/EXPECTED_UPSTREAM file. Such "
+ "dependencies are likely to be eliminated / replaced in the libcore version "
+ "already even though the new OpenJDK version still depends on them.")
public boolean usesExpectedUpstreamAsBaseDeps = false;
@Parameter(names = "-c", description = "Only show class-level dependencies, not "
+ "field-level or method-level dependency details.")
public boolean classOnly = false;
@Parameter(names = "-h", help = true, description = "Shows this help message")
public boolean help = false;
private final PrintWriter mWriter = new PrintWriter(System.out, /*autoFlush=*/true);
private void run() {
Path cp = AndroidHostEnvUtil.parseInputClasspath(classpath);
Path ecp = excludeClasspath == null || excludeClasspath.isEmpty() ? null
: AndroidHostEnvUtil.parseInputClasspath(excludeClasspath);
DependencyAnalyzer analyzer = new DependencyAnalyzer(cp, ecp,
includeInternal, usesExpectedUpstreamAsBaseDeps);
DependencyAnalyzer.Result result = analyzer.analyze(classesOrPackages);
var details = result.getDetails();
boolean isSingleClass = isInputSingleClass();
if (isSingleClass) {
mWriter.println("Input Class: " + classesOrPackages.get(0));
}
for (var classEntry : details.entrySet()) {
printMethodDependencies(isSingleClass, classEntry.getKey(), classEntry.getValue());
}
mWriter.println(" Summary:");
var collection = result.getAggregated();
printInsn(null, collection);
}
private void printMethodDependencies(boolean isSingleClass, String className,
List<MethodDependency> deps) {
boolean isClassNamePrinted = false;
for (var dep : deps) {
// Try not to flood and print tons of methods which have no dependency.
// A native method has no method and field dependency in java code, and thus
// print it when the user intends to understand the dependency of a single
// class.
if (!dep.mDependency.isEmpty()
|| (isSingleClass && (dep.mNode.access & Opcodes.ACC_NATIVE) != 0)) {
if (!isClassNamePrinted) {
mWriter.println("Class: " + className);
isClassNamePrinted = true;
}
printInsn(dep.mNode, dep.mDependency);
}
}
}
private boolean isInputSingleClass() {
if (classesOrPackages.size() != 1) {
return false;
}
String[] split = classesOrPackages.get(0).split("\\.");
String last = split[split.length - 1];
return last.length() >= 1 && Character.isUpperCase(last.charAt(0));
}
private void printInsn(MethodNode method, SignaturesCollection collection) {
PrintWriter writer = mWriter;
if (method != null) {
String printedMethod = "";
printedMethod += (method.access & Opcodes.ACC_PUBLIC) != 0 ? "public " : "";
printedMethod += (method.access & Opcodes.ACC_PROTECTED) != 0 ? "protected " : "";
printedMethod += (method.access & Opcodes.ACC_PRIVATE) != 0 ? "private " : "";
printedMethod += (method.access & Opcodes.ACC_STATIC) != 0 ? "static " : "";
printedMethod += (method.access & Opcodes.ACC_ABSTRACT) != 0 ? "abstract " : "";
printedMethod += (method.access & Opcodes.ACC_NATIVE) != 0 ? "native " : "";
printedMethod += (method.access & Opcodes.ACC_SYNTHETIC) != 0 ? "synthetic " : "";
printedMethod += method.name + method.desc;
writer.println(" Method: " + printedMethod);
}
collection.getClassStream()
.findAny()
.ifPresent(s -> writer.println(" Type dependencies:"));
collection.getClassStream()
.forEach(s -> writer.println(" " + s));
var methodStrs = collection.getMethodStream()
.map(m -> classOnly ? m.getOwner() : m.toString())
.sorted()
.distinct()
.collect(Collectors.toUnmodifiableList());
if (!methodStrs.isEmpty()) {
writer.println(" Method invokes:");
for (String s : methodStrs) {
writer.println(" " + s);
}
}
var fieldStrs = collection.getFieldStream()
.map(f -> classOnly ? f.getOwner() : f.toString())
.sorted()
.distinct()
.collect(Collectors.toUnmodifiableList());
if (!fieldStrs.isEmpty()) {
writer.println(" Field accesses:");
for (String s : fieldStrs) {
writer.println(" " + s);
}
}
writer.println();
}
}
@Parameters(commandNames = CommandListNoDeps.NAME,
commandDescription = "List classes without any dependency in the target version. "
+ "These classes in libcore/ojluni/ can be upgraded to the target version "
+ "in a standalone way without upgrading the other classes.")
private static class CommandListNoDeps {
public static final String NAME = "list-no-deps";
@Parameter(names = {"-t", "--target"}, description = "file path to a .jmod or .jar file or "
+ "one of the following OpenJDK version: 9, 11, 17")
public String classpath = "17";
@Parameter(names = "-h", help = true, description = "Shows this help message")
public boolean help = false;
private void run() throws UncheckedIOException {
if (!List.of("9", "11", "17").contains(classpath)) {
throw new IllegalArgumentException("Only 9, 11, 17 java version is supported. "
+ "This java version isn't supported: " + classpath);
}
int targetVersion = Integer.parseInt(classpath);
Path cp = AndroidHostEnvUtil.parseInputClasspath(classpath);
Path excludeClasspath = AndroidHostEnvUtil.parseInputClasspath("oj");
DependencyAnalyzer analyzer = new DependencyAnalyzer(cp, excludeClasspath,
false, true);
List<ExpectedUpstreamFile.ExpectedUpstreamEntry> entries;
try {
entries = new ExpectedUpstreamFile().readAllEntries();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
List<String> noDepClasses = new ArrayList<>();
PrintWriter writer = new PrintWriter(System.out, /*autoFlush=*/true);
for (ExpectedUpstreamFile.ExpectedUpstreamEntry entry : entries) {
String path = entry.dstPath;
if (!path.startsWith("ojluni/src/main/java/") || !path.endsWith(".java")) {
continue;
}
String jdkStr = entry.gitRef.split("/")[0];
if (jdkStr.length() < 5) {
// ignore unparsable entry
continue;
}
int jdkVersion;
try {
jdkVersion = Integer.parseInt(jdkStr.substring(
"jdk".length(), jdkStr.length() - 1));
} catch (NumberFormatException e) {
// ignore unparsable entry
continue;
}
if (jdkVersion >= targetVersion) {
continue;
}
String className = path.substring("ojluni/src/main/java/".length(),
path.length() - ".java".length()).replace('/', '.');
try {
DependencyAnalyzer.Result classResult = analyzer.analyze(List.of(className));
if (classResult.getAggregated().isEmpty()) {
noDepClasses.add(className);
}
} catch (IllegalArgumentException e) {
// Print the classes not found. The classes are either not in java.base
// or removed in the target OpenJDK version.
writer.println("Class not found: " + className + " in " + cp.toString());
}
}
writer.println("Classes with no deps: ");
for (String name : noDepClasses) {
writer.println(" " + name);
}
}
}
@Parameters(commandNames = CommandListNewApis.NAME,
commandDescription = "List the new classes / methods / fields in java.base version.")
private static class CommandListNewApis {
public static final String NAME = "list-new-apis";
@Parameter(names = {"-b", "--base"},
description = "file path to a .jmod or .jar file or"
+ "one of the following OpenJDK version: 8, 9, 11, 17")
public String base = "oj";
@Parameter(names = {"-t", "--target"},
description = "file path to a .jmod or .jar file or"
+ "one of the following OpenJDK version: 8, 9, 11, 17")
public String classpath = "17";
@Parameter(names = {"-d"},
description = "Disable the API filters read from " + UnsupportedNewApis.FILE_NAME)
public boolean disableFilter = false;
@Parameter(names = "-h", help = true, description = "Shows this help message")
public boolean help = false;
private void run() throws UncheckedIOException {
Path newClassPath = AndroidHostEnvUtil.parseInputClasspath(classpath);
Path baseClasspath = AndroidHostEnvUtil.parseInputClasspath(base);
SignaturesCollector collector = new SignaturesCollector();
// Set up filter if it's enabled.
Predicate<String> classNamePredicate;
if (disableFilter) {
classNamePredicate = (s) -> true;
} else {
UnsupportedNewApis unsupportedApis = UnsupportedNewApis.getInstance();
classNamePredicate = Predicate.not(unsupportedApis::contains);
collector.setFieldFilter(Predicate.not(unsupportedApis::contains));
collector.setMethodFilter(Predicate.not(unsupportedApis::contains));
}
try (ZipFile baseZip = new ZipFile(baseClasspath.toFile());
ZipFile newZip = new ZipFile(newClassPath.toFile())) {
Predicate<String> nameTest = Pattern.compile(
"^(classes/)?java(x)?/.*\\.class$").asMatchPredicate();
var zipEntries = newZip.entries();
while (zipEntries.hasMoreElements()) {
ZipEntry zipEntry = zipEntries.nextElement();
if (!nameTest.test(zipEntry.getName())) {
continue;
}
ClassNode newNode = readClassNode(newZip, zipEntry);
if (!isClassExposed(newZip, newNode, classNamePredicate)) {
continue;
}
ZipEntry baseEntry = ClassFileUtil.getEntryFromClassName(baseZip,
newNode.name, true);
String internalClassName = newNode.name;
// Add the class name if the entire class is missing.
if (baseEntry == null) {
collector.addClass(internalClassName);
continue;
}
ClassNode baseNode = readClassNode(baseZip, baseEntry);
DiffAnalyzer analyzer = DiffAnalyzer.analyze(baseNode, newNode);
for (FieldNode fieldNode : analyzer.newFields) {
collector.add(internalClassName, fieldNode);
}
for (MethodNode methodNode : analyzer.newMethods) {
collector.add(internalClassName, methodNode);
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
PrintWriter writer = new PrintWriter(System.out, true);
SignaturesCollection collection = collector.getCollection();
collection.getClassStream()
.forEach(writer::println);
Stream.concat(
collection.getMethodStream().map(SignaturesCollector.Method::toString),
collection.getFieldStream().map(
f -> f.getOwner() + "#" + f.getName() + ":" + f.getDesc())
).sorted().forEach(writer::println);
}
private static ClassNode readClassNode(ZipFile zipFile, ZipEntry entry) throws IOException {
try (InputStream in = zipFile.getInputStream(entry)) {
return ClassFileUtil.parseClass(in);
}
}
/**
* Return true if the class is public / protected. However, if it's inner class, it returns
* true only if the outer classes are all public / protected as well.
*/
private static boolean isClassExposed(ZipFile zipFile, ClassNode node,
Predicate<String> classNamePredicate) throws IOException {
if (!DiffAnalyzer.isExposed(node.access) || !classNamePredicate.test(node.name)) {
return false;
}
Optional<String> outerClass = node.innerClasses.stream()
.filter(inner -> node.name.equals(inner.name) && inner.outerName != null)
.map(innerClassNode -> innerClassNode.outerName)
.findFirst();
if (outerClass.isEmpty()) {
return true;
}
ZipEntry zipEntry = ClassFileUtil.getEntryFromClassName(zipFile, outerClass.get(),
true);
if (zipEntry == null) {
return true;
}
// TODO: Lookup in a cache before parsing the .class file.
try (InputStream in = zipFile.getInputStream(zipEntry)) {
ClassNode outerNode = ClassFileUtil.parseClass(in);
return isClassExposed(zipFile, outerNode, classNamePredicate);
}
}
}
public static void main(String[] argv) {
MainArgs mainArgs = new MainArgs();
CommandDump commandDump = new CommandDump();
CommandApiDiff commandApiDiff = new CommandApiDiff();
CommandShowDeps commandShowDeps = new CommandShowDeps();
CommandListNoDeps commandListNoDeps = new CommandListNoDeps();
CommandListNewApis commandListNewApis = new CommandListNewApis();
JCommander jCommander = JCommander.newBuilder()
.addObject(mainArgs)
.addCommand(commandDump)
.addCommand(commandApiDiff)
.addCommand(commandShowDeps)
.addCommand(commandListNoDeps)
.addCommand(commandListNewApis)
.build();
jCommander.parse(argv);
if (mainArgs.help || jCommander.getParsedCommand() == null) {
jCommander.usage();
return;
}
switch (jCommander.getParsedCommand()) {
case CommandDump.NAME:
if (commandDump.help) {
jCommander.usage(CommandDump.NAME);
} else {
commandDump.run();
}
break;
case CommandApiDiff.NAME:
if (commandApiDiff.help) {
jCommander.usage(CommandApiDiff.NAME);
} else {
commandApiDiff.run();
}
break;
case CommandShowDeps.NAME:
if (commandShowDeps.help) {
jCommander.usage(CommandShowDeps.NAME);
} else {
commandShowDeps.run();
}
break;
case CommandListNoDeps.NAME:
if (commandShowDeps.help) {
jCommander.usage(CommandShowDeps.NAME);
} else {
commandListNoDeps.run();
}
break;
case CommandListNewApis.NAME:
if (commandListNewApis.help) {
jCommander.usage(CommandListNewApis.NAME);
} else {
commandListNewApis.run();
}
break;
default:
throw new IllegalArgumentException("Unknown sub-command: " +
jCommander.getParsedCommand());
}
}
}