/*
* (C) Copyright Uwe Schindler (Generics Policeman) and others.
*
* 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 de.thetaphi.forbiddenapis.cli;
import static de.thetaphi.forbiddenapis.Checker.Option.*;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.net.JarURLConnection;
import java.net.URLConnection;
import java.net.URLClassLoader;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.MalformedURLException;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.codehaus.plexus.util.DirectoryScanner;
import de.thetaphi.forbiddenapis.AsmUtils;
import de.thetaphi.forbiddenapis.Checker;
import de.thetaphi.forbiddenapis.Constants;
import de.thetaphi.forbiddenapis.ForbiddenApiException;
import de.thetaphi.forbiddenapis.Logger;
import de.thetaphi.forbiddenapis.ParseException;
import de.thetaphi.forbiddenapis.StdIoLogger;
/**
* CLI class with a static main() method
*/
public final class CliMain implements Constants {
private final Option classpathOpt, dirOpt, includesOpt, excludesOpt, signaturesfileOpt, bundledsignaturesOpt, suppressannotationsOpt,
internalruntimeforbiddenOpt, allowmissingclassesOpt, allowunresolvablesignaturesOpt, versionOpt, helpOpt;
private final CommandLine cmd;
private static final Logger LOG = StdIoLogger.INSTANCE;
public static final int EXIT_SUCCESS = 0;
public static final int EXIT_VIOLATION = 1;
public static final int EXIT_ERR_CMDLINE = 2;
public static final int EXIT_UNSUPPORTED_JDK = 3;
public static final int EXIT_ERR_OTHER = 4;
public CliMain(String... args) throws ExitException {
final OptionGroup required = new OptionGroup();
required.setRequired(true);
required.addOption(dirOpt = Option.builder("d")
.desc("directory with class files to check for forbidden api usage; this directory is also added to classpath")
.longOpt("dir")
.hasArg()
.argName("directory")
.build());
required.addOption(versionOpt = Option.builder("V")
.desc("print product version and exit")
.longOpt("version")
.build());
required.addOption(helpOpt = Option.builder("h")
.desc("print this help")
.longOpt("help")
.build());
final Options options = new Options();
options.addOptionGroup(required);
options.addOption(classpathOpt = Option.builder("c")
.desc("class search path of directories and zip/jar files")
.longOpt("classpath")
.hasArgs()
.valueSeparator(File.pathSeparatorChar)
.argName("path")
.build());
options.addOption(includesOpt = Option.builder("i")
.desc("ANT-style pattern to select class files (separated by commas or option can be given multiple times, defaults to '**/*.class')")
.longOpt("includes")
.hasArgs()
.valueSeparator(',')
.argName("pattern")
.build());
options.addOption(excludesOpt = Option.builder("e")
.desc("ANT-style pattern to exclude some files from checks (separated by commas or option can be given multiple times)")
.longOpt("excludes")
.hasArgs()
.valueSeparator(',')
.argName("pattern")
.build());
options.addOption(signaturesfileOpt = Option.builder("f")
.desc("path to a file containing signatures (option can be given multiple times)")
.longOpt("signaturesfile")
.hasArg()
.argName("file")
.build());
options.addOption(bundledsignaturesOpt = Option.builder("b")
.desc("name of a bundled signatures definition (separated by commas or option can be given multiple times)")
.longOpt("bundledsignatures")
.hasArgs()
.valueSeparator(',')
.argName("name")
.build());
options.addOption(suppressannotationsOpt = Option.builder()
.desc("class name or glob pattern of annotation that suppresses error reporting in classes/methods/fields (separated by commas or option can be given multiple times)")
.longOpt("suppressannotation")
.hasArgs()
.valueSeparator(',')
.argName("classname")
.build());
options.addOption(internalruntimeforbiddenOpt = Option.builder()
.desc(String.format(Locale.ENGLISH, "DEPRECATED: forbids calls to non-portable runtime APIs; use bundled signatures '%s' instead", BS_JDK_NONPORTABLE))
.longOpt("internalruntimeforbidden")
.build());
options.addOption(allowmissingclassesOpt = Option.builder()
.desc("don't fail if a referenced class is missing on classpath")
.longOpt("allowmissingclasses")
.build());
options.addOption(allowunresolvablesignaturesOpt = Option.builder()
.desc("don't fail if a signature is not resolving")
.longOpt("allowunresolvablesignatures")
.build());
try {
this.cmd = new DefaultParser().parse(options, args);
if (cmd.hasOption(helpOpt.getLongOpt())) {
printHelp(options);
throw new ExitException(EXIT_SUCCESS);
}
if (cmd.hasOption(versionOpt.getLongOpt())) {
printVersion();
throw new ExitException(EXIT_SUCCESS);
}
} catch (org.apache.commons.cli.ParseException pe) {
printHelp(options);
throw new ExitException(EXIT_ERR_CMDLINE);
}
}
private void printVersion() {
final Package pkg = this.getClass().getPackage();
LOG.info(String.format(Locale.ENGLISH,
"%s %s",
pkg.getImplementationTitle(), pkg.getImplementationVersion()
));
}
private void printHelp(Options options) {
final HelpFormatter formatter = new HelpFormatter();
String clazzName = getClass().getName();
String cmdline = "java " + clazzName;
try {
final URLConnection conn = getClass().getClassLoader().getResource(AsmUtils.getClassResourceName(clazzName)).openConnection();
if (conn instanceof JarURLConnection) {
final URL jarUrl = ((JarURLConnection) conn).getJarFileURL();
if ("file".equalsIgnoreCase(jarUrl.getProtocol())) {
final String cwd = new File(".").getCanonicalPath(), path = new File(jarUrl.toURI()).getCanonicalPath();
cmdline = "java -jar " + (path.startsWith(cwd) ? path.substring(cwd.length() + File.separator.length()) : path);
}
}
} catch (IOException ioe) {
// ignore, use default cmdline value
} catch (URISyntaxException use) {
// ignore, use default cmdline value
}
formatter.printHelp(cmdline + " [options]",
"Scans a set of class files for forbidden API usage.",
options,
String.format(Locale.ENGLISH,
"Exit codes: %d = SUCCESS, %d = forbidden API detected, %d = invalid command line, %d = unsupported JDK version, %d = other error (I/O,...)",
EXIT_SUCCESS, EXIT_VIOLATION, EXIT_ERR_CMDLINE, EXIT_UNSUPPORTED_JDK, EXIT_ERR_OTHER
)
);
}
public void run() throws ExitException {
final File classesDirectory = new File(cmd.getOptionValue(dirOpt.getLongOpt())).getAbsoluteFile();
// parse classpath given as argument; add -d to classpath, too
final String[] classpath = cmd.getOptionValues(classpathOpt.getLongOpt());
final URL[] urls;
try {
if (classpath == null) {
urls = new URL[] { classesDirectory.toURI().toURL() };
} else {
urls = new URL[classpath.length + 1];
int i = 0;
for (final String cpElement : classpath) {
urls[i++] = new File(cpElement).toURI().toURL();
}
urls[i++] = classesDirectory.toURI().toURL();
assert i == urls.length;
}
} catch (MalformedURLException mfue) {
throw new ExitException(EXIT_ERR_OTHER, "The given classpath is invalid: " + mfue);
}
// System.err.println("Classpath: " + Arrays.toString(urls));
final URLClassLoader loader = URLClassLoader.newInstance(urls, ClassLoader.getSystemClassLoader());
try {
final EnumSet<Checker.Option> options = EnumSet.of(FAIL_ON_VIOLATION);
if (!cmd.hasOption(allowmissingclassesOpt.getLongOpt())) options.add(FAIL_ON_MISSING_CLASSES);
if (!cmd.hasOption(allowunresolvablesignaturesOpt.getLongOpt())) options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
final Checker checker = new Checker(LOG, loader, options);
if (!checker.isSupportedJDK) {
throw new ExitException(EXIT_UNSUPPORTED_JDK, String.format(Locale.ENGLISH,
"Your Java runtime (%s %s) is not supported by forbiddenapis. Please run the checks with a supported JDK!",
System.getProperty("java.runtime.name"), System.getProperty("java.runtime.version")));
}
final String[] suppressAnnotations = cmd.getOptionValues(suppressannotationsOpt.getLongOpt());
if (suppressAnnotations != null) for (String a : suppressAnnotations) {
checker.addSuppressAnnotation(a);
}
LOG.info("Scanning for classes to check...");
if (!classesDirectory.exists()) {
throw new ExitException(EXIT_ERR_OTHER, "Directory with class files does not exist: " + classesDirectory);
}
String[] includes = cmd.getOptionValues(includesOpt.getLongOpt());
if (includes == null || includes.length == 0) {
includes = new String[] { "**/*.class" };
}
final String[] excludes = cmd.getOptionValues(excludesOpt.getLongOpt());
final DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(classesDirectory);
ds.setCaseSensitive(true);
ds.setIncludes(includes);
ds.setExcludes(excludes);
ds.addDefaultExcludes();
ds.scan();
final String[] files = ds.getIncludedFiles();
if (files.length == 0) {
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
"No classes found in directory %s (includes=%s, excludes=%s).",
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
}
try {
final String[] bundledSignatures = cmd.getOptionValues(bundledsignaturesOpt.getLongOpt());
if (bundledSignatures != null) for (String bs : new LinkedHashSet<String>(Arrays.asList(bundledSignatures))) {
checker.addBundledSignatures(bs, null);
}
if (cmd.hasOption(internalruntimeforbiddenOpt.getLongOpt())) {
LOG.warn(DEPRECATED_WARN_INTERNALRUNTIME);
checker.addBundledSignatures(BS_JDK_NONPORTABLE, null);
}
final String[] signaturesFiles = cmd.getOptionValues(signaturesfileOpt.getLongOpt());
if (signaturesFiles != null) for (String sf : new LinkedHashSet<String>(Arrays.asList(signaturesFiles))) {
final File f = new File(sf).getAbsoluteFile();
checker.parseSignaturesFile(f);
}
} catch (IOException ioe) {
throw new ExitException(EXIT_ERR_OTHER, "IO problem while reading files with API signatures: " + ioe);
} catch (ParseException pe) {
throw new ExitException(EXIT_ERR_OTHER, "Parsing signatures failed: " + pe.getMessage());
}
if (checker.hasNoSignatures()) {
throw new ExitException(EXIT_ERR_CMDLINE, String.format(Locale.ENGLISH,
"No API signatures found; use parameters '--%s' and/or '--%s' to specify those!",
bundledsignaturesOpt.getLongOpt(), signaturesfileOpt.getLongOpt()
));
}
try {
checker.addClassesToCheck(classesDirectory, files);
} catch (IOException ioe) {
throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe);
}
try {
checker.run();
} catch (ForbiddenApiException fae) {
throw new ExitException(EXIT_VIOLATION, fae.getMessage());
}
} finally {
// Java 7 supports closing URLClassLoader, so check for Closeable interface:
if (loader instanceof Closeable) try {
((Closeable) loader).close();
} catch (IOException ioe) {
// ignore
}
}
}
public static void main(String... args) {
try {
new CliMain(args).run();
} catch (ExitException e) {
if (e.getMessage() != null) {
LOG.error(e.getMessage());
}
if (e.exitCode != 0) {
System.exit(e.exitCode);
}
}
}
}