/* * (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.maven; import static de.thetaphi.forbiddenapis.Checker.Option.*; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.codehaus.plexus.util.DirectoryScanner; 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 java.io.Closeable; import java.io.File; import java.io.IOException; import java.lang.annotation.RetentionPolicy; import java.net.URI; import java.net.URISyntaxException; import java.net.URLClassLoader; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; /** * Base class for forbiddenapis Mojos. * @since 1.0 */ public abstract class AbstractCheckMojo extends AbstractMojo implements Constants { /** * Lists all files, which contain signatures and comments for forbidden API calls. * The signatures are resolved against the compile classpath. * @since 1.0 */ @Parameter(required = false) private File[] signaturesFiles; /** * Lists all Maven artifacts, which contain signatures and comments for forbidden API calls. * The artifact needs to be specified like a Maven dependency. Resolution is not transitive. * You can refer to plain text Maven artifacts ({@code type="txt"}, e.g., with a separate {@code classifier}): * <pre> * <signaturesArtifact> * <groupId>org.apache.foobar</groupId> * <artifactId>example</artifactId> * <version>1.0</version> * <classifier>signatures</classifier> * <type>txt</type> * </signaturesArtifact> * </pre> * Alternatively, refer to signatures files inside JAR artifacts. In that case, the additional * parameter {@code path} has to be given: * <pre> * <signaturesArtifact> * <groupId>org.apache.foobar</groupId> * <artifactId>example</artifactId> * <version>1.0</version> * <type>jar</type> * <path>path/inside/jar/file/signatures.txt</path> * </signaturesArtifact> * </pre> * <p>The signatures are resolved against the compile classpath. * @since 2.0 */ @Parameter(required = false) private SignaturesArtifact[] signaturesArtifacts; /** * Gives a multiline list of signatures, inline in the pom.xml. Use an XML CDATA section to do that! * The signatures are resolved against the compile classpath. * @since 1.0 */ @Parameter(required = false) private String signatures; /** * Specifies <a href="bundled-signatures.html">built-in signatures</a> files (e.g., deprecated APIs for specific Java versions, * unsafe method calls using default locale, default charset,...) * @since 1.0 */ @Parameter(required = false) private String[] bundledSignatures; /** * Forbids calls to non-portable runtime APIs (like {@code sun.misc.Unsafe}). * <em>Please note:</em> This enables {@code "jdk-non-portable"} bundled signatures for backwards compatibility. * @deprecated Use <a href="bundled-signatures.html">bundled signatures</a> {@code "jdk-non-portable"} or {@code "jdk-internal"} instead. * @since 1.0 */ @Deprecated @Parameter(required = false, defaultValue = "false") private boolean internalRuntimeForbidden; /** * Fail the build, if the bundled ASM library cannot read the class file format * of the runtime library or the runtime library cannot be discovered. * @since 1.0 */ @Parameter(required = false, defaultValue = "false") private boolean failOnUnsupportedJava; /** * Fail the build, if a class referenced in the scanned code is missing. This requires * that you pass the whole classpath including all dependencies to this Mojo * (Maven does this by default). * @since 1.0 */ @Parameter(required = false, defaultValue = "true") private boolean failOnMissingClasses; /** * Fail the build if a signature is not resolving. If this parameter is set to * to false, then such signatures are silently ignored. This is useful in multi-module Maven * projects where only some modules have the dependency to which the signature file(s) apply. * @since 1.4 */ @Parameter(required = false, defaultValue = "true") private boolean failOnUnresolvableSignatures; /** * Fail the build if violations have been found. Defaults to {@code true}. * @since 2.0 */ @Parameter(required = false, property="forbiddenapis.failOnViolation", defaultValue = "true") private boolean failOnViolation; /** * Disable the internal JVM classloading cache when getting bytecode from * the classpath. This setting slows down checks, but <em>may</em> work around * issues with other Mojos, that do not close their class loaders. * If you get {@code FileNotFoundException}s related to non-existent JAR entries * you can try to work around using this setting. * @since 2.2 */ @Parameter(required = false, defaultValue = "false") private boolean disableClassloadingCache; /** * The default compiler target version used to expand references to bundled JDK signatures. * E.g., if you use "jdk-deprecated", it will expand to this version. * This setting should be identical to the target version used in the compiler plugin. * @since 1.0 */ @Parameter(required = false, defaultValue = "${maven.compiler.target}") private String targetVersion; /** * List of patterns matching all class files to be parsed from the classesDirectory. * Can be changed to e.g. exclude several files (using excludes). * The default is a single include with pattern '**/*.class' * @see #excludes * @since 1.0 */ @Parameter(required = false) private String[] includes; /** * List of patterns matching class files to be excluded from checking. * @see #includes * @since 1.0 */ @Parameter(required = false) private String[] excludes; /** * List of a custom Java annotations (full class names) that are used in the checked * code to suppress errors. Those annotations must have at least * {@link RetentionPolicy#CLASS}. They can be applied to classes, their methods, * or fields. By default, {@code @de.thetaphi.forbiddenapis.SuppressForbidden} * can always be used, but needs the {@code forbidden-apis.jar} file in classpath * of compiled project, which may not be wanted. * Instead of a full class name, a glob pattern may be used (e.g., * {@code **.SuppressForbidden}). * @since 1.8 */ @Parameter(required = false) private String[] suppressAnnotations; /** * Skip entire check. Most useful on the command line via "-Dforbiddenapis.skip=true". * @since 1.6 */ @Parameter(required = false, property="forbiddenapis.skip", defaultValue="false") private boolean skip; /** The project packaging (pom, jar, etc.). */ @Parameter(defaultValue = "${project.packaging}", readonly = true, required = true) private String packaging; @Component private ArtifactFactory artifactFactory; @Component private ArtifactResolver artifactResolver; @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true) private List<ArtifactRepository> remoteRepositories; @Parameter(defaultValue = "${localRepository}", readonly = true, required = true) private ArtifactRepository localRepository; /** provided by the concrete Mojos for compile and test classes processing */ protected abstract List<String> getClassPathElements(); /** provided by the concrete Mojos for compile and test classes processing */ protected abstract File getClassesDirectory(); /** gets overridden for test, because it uses testTargetVersion as optional name to override */ protected String getTargetVersion() { return targetVersion; } private File resolveSignaturesArtifact(SignaturesArtifact signaturesArtifact) throws ArtifactResolutionException, ArtifactNotFoundException { final Artifact artifact = signaturesArtifact.createArtifact(artifactFactory); artifactResolver.resolve(artifact, this.remoteRepositories, this.localRepository); final File f = artifact.getFile(); // Can this ever be false? Be sure. Found the null check also in other Maven code, so be safe! if (f == null) { throw new ArtifactNotFoundException("Artifact does not resolve to a file.", artifact); } return f; } private String encodeUrlPath(String path) { try { // hack to encode the URL path by misusing URI class: return new URI(null, path, null).toASCIIString(); } catch (URISyntaxException e) { throw new IllegalArgumentException(e.getMessage()); } } private URL createJarUrl(File f, String jarPath) throws MalformedURLException { final URL fileUrl = f.toURI().toURL(); final URL jarBaseUrl = new URL("jar", null, fileUrl.toExternalForm() + "!/"); return new URL(jarBaseUrl, encodeUrlPath(jarPath)); } @Override public void execute() throws MojoExecutionException { final Logger log = new Logger() { @Override public void error(String msg) { getLog().error(msg); } @Override public void warn(String msg) { getLog().warn(msg); } @Override public void info(String msg) { getLog().info(msg); } }; if (skip) { log.info("Skipping forbidden-apis checks."); return; } // In multi-module projects, one may want to configure the plugin in the parent/root POM. // However, it should not be executed for this type of POMs. if ("pom".equals(packaging)) { log.info("Skipping execution for packaging \"" + packaging + "\""); return; } // set default param: if (includes == null) includes = new String[] {"**/*.class"}; final List<String> cp = getClassPathElements(); final URL[] urls = new URL[cp.size()]; try { int i = 0; for (final String cpElement : cp) { urls[i++] = new File(cpElement).toURI().toURL(); } assert i == urls.length; } catch (MalformedURLException e) { throw new MojoExecutionException("Failed to build classpath.", e); } URLClassLoader urlLoader = null; final ClassLoader loader = (urls.length > 0) ? (urlLoader = URLClassLoader.newInstance(urls, ClassLoader.getSystemClassLoader())) : ClassLoader.getSystemClassLoader(); try { final EnumSet<Checker.Option> options = EnumSet.noneOf(Checker.Option.class); if (failOnMissingClasses) options.add(FAIL_ON_MISSING_CLASSES); if (failOnViolation) options.add(FAIL_ON_VIOLATION); if (failOnUnresolvableSignatures) options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES); if (disableClassloadingCache) options.add(DISABLE_CLASSLOADING_CACHE); final Checker checker = new Checker(log, loader, options); if (!checker.isSupportedJDK) { final String msg = String.format(Locale.ENGLISH, "Your Java runtime (%s %s) is not supported by the forbiddenapis MOJO. Please run the checks with a supported JDK!", System.getProperty("java.runtime.name"), System.getProperty("java.runtime.version")); if (failOnUnsupportedJava) { throw new MojoExecutionException(msg); } else { log.warn(msg); return; } } if (suppressAnnotations != null) { for (String a : suppressAnnotations) { checker.addSuppressAnnotation(a); } } log.info("Scanning for classes to check..."); final File classesDirectory = getClassesDirectory(); if (!classesDirectory.exists()) { log.warn("Classes directory does not exist, forbiddenapis check skipped: " + classesDirectory); return; } 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) { log.warn(String.format(Locale.ENGLISH, "No classes found in '%s' (includes=%s, excludes=%s), forbiddenapis check skipped.", classesDirectory.toString(), Arrays.toString(includes), Arrays.toString(excludes))); return; } try { if (bundledSignatures != null) { String targetVersion = getTargetVersion(); if ("".equals(targetVersion)) targetVersion = null; if (targetVersion == null) { log.warn("The 'targetVersion' parameter or '${maven.compiler.target}' property is missing. " + "Trying to read bundled JDK signatures without compiler target. " + "You have to explicitely specify the version in the resource name."); } for (String bs : new LinkedHashSet<String>(Arrays.asList(bundledSignatures))) { checker.addBundledSignatures(bs, targetVersion); } } if (internalRuntimeForbidden) { log.warn(DEPRECATED_WARN_INTERNALRUNTIME); checker.addBundledSignatures(BS_JDK_NONPORTABLE, null); } final Set<File> sigFiles = new LinkedHashSet<File>(); final Set<URL> sigUrls = new LinkedHashSet<URL>(); if (signaturesFiles != null) { sigFiles.addAll(Arrays.asList(signaturesFiles)); } if (signaturesArtifacts != null) { for (final SignaturesArtifact artifact : signaturesArtifacts) { final File f = resolveSignaturesArtifact(artifact); if (artifact.path != null) { if (f.isDirectory()) { // if Maven did not yet jarred the artifact, it returns the classes // folder of the foreign Maven project, just use that one: sigFiles.add(new File(f, artifact.path)); } else { sigUrls.add(createJarUrl(f, artifact.path)); } } else { sigFiles.add(f); } } } for (final File f : sigFiles) { checker.parseSignaturesFile(f); } for (final URL u : sigUrls) { checker.parseSignaturesFile(u); } final String sig = (signatures != null) ? signatures.trim() : null; if (sig != null && sig.length() != 0) { checker.parseSignaturesString(sig); } } catch (IOException ioe) { throw new MojoExecutionException("IO problem while reading files with API signatures.", ioe); } catch (ParseException pe) { throw new MojoExecutionException("Parsing signatures failed: " + pe.getMessage(), pe); } catch (ArtifactResolutionException e) { throw new MojoExecutionException("Problem while resolving Maven artifact.", e); } catch (ArtifactNotFoundException e) { throw new MojoExecutionException("Maven artifact does not exist.", e); } if (checker.hasNoSignatures()) { if (failOnUnresolvableSignatures) { throw new MojoExecutionException("No API signatures found; use parameters 'signatures', 'bundledSignatures', 'signaturesFiles', and/or 'signaturesArtifacts' to define those!"); } else { log.info("Skipping execution because no API signatures are available."); return; } } try { checker.addClassesToCheck(classesDirectory, files); } catch (IOException ioe) { throw new MojoExecutionException("Failed to load one of the given class files.", ioe); } try { checker.run(); } catch (ForbiddenApiException fae) { throw new MojoExecutionException(fae.getMessage(), fae.getCause()); } } finally { // Java 7 supports closing URLClassLoader, so check for Closeable interface: if (urlLoader instanceof Closeable) try { ((Closeable) urlLoader).close(); } catch (IOException ioe) { // ignore } } } }