/*
* (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
}
}
}
}