package japicmp.cli; import com.google.common.base.Optional; import io.airlift.airline.Command; import io.airlift.airline.HelpOption; import io.airlift.airline.Option; import japicmp.cmp.JApiCmpArchive; import japicmp.cmp.JarArchiveComparator; import japicmp.cmp.JarArchiveComparatorOptions; import japicmp.config.Options; import japicmp.exception.JApiCmpException; import japicmp.model.AccessModifier; import japicmp.model.JApiClass; import japicmp.output.semver.SemverOut; import japicmp.output.stdout.StdoutOutputGenerator; import japicmp.output.xml.XmlOutput; import japicmp.output.xml.XmlOutputGenerator; import japicmp.output.xml.XmlOutputGeneratorOptions; import javax.inject.Inject; import java.io.File; import java.util.ArrayList; import java.util.List; public class JApiCli { public static final String IGNORE_MISSING_CLASSES = "--ignore-missing-classes"; public static final String IGNORE_MISSING_CLASSES_BY_REGEX = "--ignore-missing-classes-by-regex"; public static final String OLD_CLASSPATH = "--old-classpath"; public static final String NEW_CLASSPATH = "--new-classpath"; public enum ClassPathMode { ONE_COMMON_CLASSPATH, TWO_SEPARATE_CLASSPATHS } @Command(name = "java -jar japicmp.jar", description = "Compares jars") public static class Compare implements Runnable { @Inject public HelpOption helpOption; @Option(name = { "-o", "--old" }, description = "Provides the path to the old version(s) of the jar(s). Use ; to separate jar files.") public String pathToOldVersionJar; @Option(name = { "-n", "--new" }, description = "Provides the path to the new version(s) of the jar(s). Use ; to separate jar files.") public String pathToNewVersionJar; @Option(name = { "-m", "--only-modified" }, description = "Outputs only modified classes/methods.") public boolean modifiedOnly; @Option(name = { "-b", "--only-incompatible" }, description = "Outputs only classes/methods that are binary incompatible. If not given, all classes and methods are printed.") public boolean onlyBinaryIncompatibleModifications; @Option(name = "-a", description = "Sets the access modifier level (public, package, protected, private), which should be used.") public String accessModifier; @Option(name = {"-i", "--include"}, description = "Semicolon separated list of elements to include in the form package.Class#classMember, * can be used as wildcard. Annotations are given as FQN starting with @. Examples: mypackage;my.Class;other.Class#method(int,long);foo.Class#field;@my.Annotation.") public String includes; @Option(name = {"-e", "--exclude"}, description = "Semicolon separated list of elements to exclude in the form package.Class#classMember, * can be used as wildcard. Annotations are given as FQN starting with @. Examples: mypackage;my.Class;other.Class#method(int,long);foo.Class#field;@my.Annotation.") public String excludes; @Option(name = { "-x", "--xml-file" }, description = "Provides the path to the xml output file.") public String pathToXmlOutputFile; @Option(name = { "--html-file" }, description = "Provides the path to the html output file.") public String pathToHtmlOutputFile; @Option(name = { "-s", "--semantic-versioning" }, description = "Tells you which part of the version to increment.") public boolean semanticVersioning = false; @Option(name = { "--include-synthetic" }, description = "Include synthetic classes and class members that are hidden per default.") public boolean includeSynthetic = false; @Option(name = { IGNORE_MISSING_CLASSES }, description = "Ignores all superclasses/interfaces missing on the classpath.") public boolean ignoreMissingClasses = false; @Option(name = {IGNORE_MISSING_CLASSES_BY_REGEX}, description = "Ignores only those superclasses/interface missing on the classpath that are selected by a regular expression.") public List<String> ignoreMissingClassesByRegEx = new ArrayList<>(); @Option(name = { "--html-stylesheet" }, description = "Provides the path to your own stylesheet.") public String pathToHtmlStylesheet; @Option(name = { OLD_CLASSPATH }, description = "The classpath for the old version.") public String oldClassPath; @Option(name = { NEW_CLASSPATH }, description = "The classpath for the new version.") public String newClassPath; @Option(name = "--no-annotations", description = "Do not evaluate annotations.") public boolean noAnnotations = false; @Option(name = "--report-only-filename", description = "Use just filename in report description.") public boolean reportOnlyFilename; @Override public void run() { Options options = createOptionsFromCliArgs(); JarArchiveComparator jarArchiveComparator = new JarArchiveComparator(JarArchiveComparatorOptions.of(options)); List<JApiClass> jApiClasses = jarArchiveComparator.compare(options.getOldArchives(), options.getNewArchives()); generateOutput(options, jApiClasses); } private void generateOutput(Options options, List<JApiClass> jApiClasses) { if (semanticVersioning) { SemverOut semverOut = new SemverOut(options, jApiClasses); String output = semverOut.generate(); System.out.println(output); return; } if (options.getXmlOutputFile().isPresent() || options.getHtmlOutputFile().isPresent()) { SemverOut semverOut = new SemverOut(options, jApiClasses); XmlOutputGeneratorOptions xmlOutputGeneratorOptions = new XmlOutputGeneratorOptions(); xmlOutputGeneratorOptions.setCreateSchemaFile(true); xmlOutputGeneratorOptions.setSemanticVersioningInformation(semverOut.generate()); XmlOutputGenerator xmlGenerator = new XmlOutputGenerator(jApiClasses, options, xmlOutputGeneratorOptions); try (XmlOutput xmlOutput = xmlGenerator.generate()) { XmlOutputGenerator.writeToFiles(options, xmlOutput); } catch (Exception e) { throw new JApiCmpException(JApiCmpException.Reason.IoException, "Could not close output streams: " + e.getMessage(), e); } } StdoutOutputGenerator stdoutOutputGenerator = new StdoutOutputGenerator(options, jApiClasses); String output = stdoutOutputGenerator.generate(); System.out.println(output); } private Options createOptionsFromCliArgs() { Options options = Options.newDefault(); options.getOldArchives().addAll(createFileList(checkNonNull(pathToOldVersionJar, "Required option -o is missing."))); options.getNewArchives().addAll(createFileList(checkNonNull(pathToNewVersionJar, "Required option -n is missing."))); options.setXmlOutputFile(Optional.fromNullable(pathToXmlOutputFile)); options.setHtmlOutputFile(Optional.fromNullable(pathToHtmlOutputFile)); options.setOutputOnlyModifications(modifiedOnly); options.setAccessModifier(toModifier(accessModifier)); options.addIncludeFromArgument(Optional.fromNullable(includes)); options.addExcludeFromArgument(Optional.fromNullable(excludes)); options.setOutputOnlyBinaryIncompatibleModifications(onlyBinaryIncompatibleModifications); options.setIncludeSynthetic(includeSynthetic); options.setIgnoreMissingClasses(ignoreMissingClasses); options.setHtmlStylesheet(Optional.fromNullable(pathToHtmlStylesheet)); options.setOldClassPath(Optional.fromNullable(oldClassPath)); options.setNewClassPath(Optional.fromNullable(newClassPath)); options.setNoAnnotations(noAnnotations); for (String missingClassRegEx : ignoreMissingClassesByRegEx) { options.addIgnoreMissingClassRegularExpression(missingClassRegEx); } options.setReportOnlyFilename(reportOnlyFilename); options.verify(); return options; } private List<JApiCmpArchive> createFileList(String option) { String[] parts = option.split(";"); List<JApiCmpArchive> jApiCmpArchives = new ArrayList<>(parts.length); for (String part : parts) { File file = new File(part); JApiCmpArchive jApiCmpArchive = new JApiCmpArchive(file, "n.a."); jApiCmpArchives.add(jApiCmpArchive); } return jApiCmpArchives; } private <T> T checkNonNull(T in, String errorMessage) { if (in == null) { throw new JApiCmpException(JApiCmpException.Reason.CliError, errorMessage); } else { return in; } } private Optional<AccessModifier> toModifier(String accessModifierArg) { Optional<String> stringOptional = Optional.fromNullable(accessModifierArg); if (stringOptional.isPresent()) { try { return Optional.of(AccessModifier.valueOf(stringOptional.get().toUpperCase())); } catch (IllegalArgumentException e) { throw new JApiCmpException(JApiCmpException.Reason.CliError, String.format("Invalid value for option -a: %s. Possible values are: %s.", accessModifierArg, AccessModifier.listOfAccessModifier()), e); } } else { return Optional.of(AccessModifier.PROTECTED); } } } }