Java tutorial
package de.thetaphi.forbiddenapis; /* * (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. */ import static de.thetaphi.forbiddenapis.Checker.Option.*; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.EnumSet; 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.HelpFormatter; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.PosixParser; import org.codehaus.plexus.util.DirectoryScanner; /** * CLI class with a static main() method */ public final class CliMain { private final Option classpathOpt, dirOpt, includesOpt, excludesOpt, signaturesfileOpt, bundledsignaturesOpt, suppressannotationsOpt, internalruntimeforbiddenOpt, allowmissingclassesOpt, allowunresolvablesignaturesOpt, versionOpt, helpOpt; private final CommandLine cmd; 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; @SuppressWarnings({ "static-access", "static" }) public CliMain(String... args) throws ExitException { final OptionGroup required = new OptionGroup(); required.setRequired(true); required.addOption(dirOpt = OptionBuilder.withDescription( "directory with class files to check for forbidden api usage; this directory is also added to classpath") .withLongOpt("dir").hasArg().withArgName("directory").create('d')); required.addOption(versionOpt = OptionBuilder.withDescription("print product version and exit") .withLongOpt("version").create('V')); required.addOption( helpOpt = OptionBuilder.withDescription("print this help").withLongOpt("help").create('h')); final Options options = new Options(); options.addOptionGroup(required); options.addOption(classpathOpt = OptionBuilder .withDescription("class search path of directories and zip/jar files").withLongOpt("classpath") .hasArgs().withValueSeparator(File.pathSeparatorChar).withArgName("path").create('c')); options.addOption(includesOpt = OptionBuilder.withDescription( "ANT-style pattern to select class files (separated by commas or option can be given multiple times, defaults to '**/*.class')") .withLongOpt("includes").hasArgs().withValueSeparator(',').withArgName("pattern").create('i')); options.addOption(excludesOpt = OptionBuilder.withDescription( "ANT-style pattern to exclude some files from checks (separated by commas or option can be given multiple times)") .withLongOpt("excludes").hasArgs().withValueSeparator(',').withArgName("pattern").create('e')); options.addOption(signaturesfileOpt = OptionBuilder .withDescription("path to a file containing signatures (option can be given multiple times)") .withLongOpt("signaturesfile").hasArg().withArgName("file").create('f')); options.addOption(bundledsignaturesOpt = OptionBuilder.withDescription( "name of a bundled signatures definition (separated by commas or option can be given multiple times)") .withLongOpt("bundledsignatures").hasArgs().withValueSeparator(',').withArgName("name") .create('b')); options.addOption(suppressannotationsOpt = OptionBuilder.withDescription( "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)") .withLongOpt("suppressannotation").hasArgs().withValueSeparator(',').withArgName("classname") .create()); options.addOption(internalruntimeforbiddenOpt = OptionBuilder .withDescription("forbids calls to classes from the internal java runtime (like sun.misc.Unsafe)") .withLongOpt("internalruntimeforbidden").create()); options.addOption(allowmissingclassesOpt = OptionBuilder .withDescription("don't fail if a referenced class is missing on classpath") .withLongOpt("allowmissingclasses").create()); options.addOption(allowunresolvablesignaturesOpt = OptionBuilder .withDescription("don't fail if a signature is not resolving") .withLongOpt("allowunresolvablesignatures").create()); try { this.cmd = new PosixParser().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); } } @SuppressForbidden void logError(String msg) { System.err.println("ERROR: " + msg); } @SuppressForbidden void logWarn(String msg) { System.err.println("WARNING: " + msg); } @SuppressForbidden void logInfo(String msg) { System.out.println(msg); } private void printVersion() { final Package pkg = this.getClass().getPackage(); logInfo(String.format(Locale.ENGLISH, "%s %s", pkg.getImplementationTitle(), pkg.getImplementationVersion())); } private void printHelp(Options options) { final HelpFormatter formatter = new HelpFormatter(); String cmdline = "java " + getClass().getName(); try { final URLConnection conn = getClass().getResource(getClass().getSimpleName() + ".class") .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(internalruntimeforbiddenOpt.getLongOpt())) options.add(INTERNAL_RUNTIME_FORBIDDEN); 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(loader, options) { @Override protected void logError(String msg) { CliMain.this.logError(msg); } @Override protected void logWarn(String msg) { CliMain.this.logWarn(msg); } @Override protected void logInfo(String msg) { CliMain.this.logInfo(msg); } }; 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); } logInfo("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 : bundledSignatures) { logInfo("Reading bundled API signatures: " + bs); checker.parseBundledSignatures(bs, null); } final String[] signaturesFiles = cmd.getOptionValues(signaturesfileOpt.getLongOpt()); if (signaturesFiles != null) for (String sf : signaturesFiles) { final File f = new File(sf).getAbsoluteFile(); logInfo("Reading API signatures: " + f); checker.parseSignaturesFile(new FileInputStream(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', '--%s', and/or '--%s' to define those!", bundledsignaturesOpt.getLongOpt(), signaturesfileOpt.getLongOpt(), internalruntimeforbiddenOpt.getLongOpt())); } logInfo("Loading classes to check..."); try { for (String f : files) { checker.addClassToCheck(new FileInputStream(new File(classesDirectory, f))); } } catch (IOException ioe) { throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe); } logInfo("Scanning for API signatures and dependencies..."); 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 } } } @SuppressWarnings("serial") public static final class ExitException extends Exception { public final int exitCode; public ExitException(int exitCode) { this(exitCode, null); } public ExitException(int exitCode, String message) { super(message); this.exitCode = exitCode; } } @SuppressForbidden public static void main(String... args) { try { new CliMain(args).run(); } catch (ExitException e) { if (e.getMessage() != null) { System.err.println("ERROR: " + e.getMessage()); } if (e.exitCode != 0) { System.exit(e.exitCode); } } } }