/*
* (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.gradle;
import static de.thetaphi.forbiddenapis.Checker.Option.*;
import groovy.lang.Closure;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.RetentionPolicy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Incubating;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.resources.ResourceException;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.ParallelizableTask;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationTask;
import org.gradle.api.tasks.util.PatternFilterable;
import org.gradle.api.tasks.util.PatternSet;
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;
/**
* <h3>ForbiddenApis Gradle Task (requires at least Gradle v2.3)</h3>
* <p>
* The plugin registers a separate task for each defined {@code sourceSet} using
* the default task naming convention. For default Java projects, two tasks are created:
* {@code forbiddenApisMain} and {@code forbiddenApisTest}. Additional source sets
* will produce a task with similar names ({@code 'forbiddenApis' + nameOfSourceSet}).
* All tasks are added as dependencies to the {@code check} default Gradle task.
* For convenience, the plugin also defines an additional task {@code forbiddenApis}
* that runs checks on all source sets.
* <p>
* Installation can be done from your {@code build.gradle} file:
* <pre>
* buildscript {
* repositories {
* mavenCentral()
* }
* dependencies {
* classpath 'de.thetaphi:forbiddenapis:' + FORBIDDEN_APIS_VERSION
* }
* }
*
* apply plugin: 'java'
* apply plugin: 'de.thetaphi.forbiddenapis'
* </pre>
* After that you can add the following task configuration closures:
* <pre>
* forbiddenApisMain {
* bundledSignatures += 'jdk-system-out'
* }
* </pre>
* <em>(using the {@code '+='} notation, you can add additional bundled signatures to the defaults).</em>
* <p>
* To define those defaults, which are used by all source sets, you can use the
* extension / convention mapping provided by {@link CheckForbiddenApisExtension}:
* <pre>
* forbiddenApis {
* bundledSignatures = [ 'jdk-unsafe', 'jdk-deprecated' ]
* signaturesFiles = files('path/to/my/signatures.txt')
* ignoreFailures = false
* }
* </pre>
*
* @since 2.0
*/
@ParallelizableTask
public class CheckForbiddenApis extends DefaultTask implements PatternFilterable,VerificationTask,Constants {
private static final String NL = System.getProperty("line.separator", "\n");
private final CheckForbiddenApisExtension data = new CheckForbiddenApisExtension();
private final PatternSet patternSet = new PatternSet().include("**/*.class");
private File classesDir;
private FileCollection classpath;
private String targetCompatibility;
/**
* Directory with the class files to check.
* Defaults to current sourseSet's output directory.
*/
@OutputDirectory
// no @InputDirectory, we use separate getter for a list of all input files
public File getClassesDir() {
return classesDir;
}
/** @see #getClassesDir */
public void setClassesDir(File classesDir) {
this.classesDir = classesDir;
}
/** Returns the pattern set to match against class files in {@link #getClassesDir()}. */
public PatternSet getPatternSet() {
return patternSet;
}
/** @see #getPatternSet() */
public void setPatternSet(PatternSet patternSet) {
patternSet.copyFrom(patternSet);
}
/**
* A {@link FileCollection} used to configure the classpath.
* Defaults to current sourseSet's compile classpath.
*/
@InputFiles
public FileCollection getClasspath() {
return classpath;
}
/** @see #getClasspath */
public void setClasspath(FileCollection classpath) {
this.classpath = classpath;
}
/**
* A {@link FileCollection} containing all files, which contain signatures and comments for forbidden API calls.
* The signatures are resolved against {@link #getClasspath()}.
*/
@InputFiles
@Optional
public FileCollection getSignaturesFiles() {
return data.signaturesFiles;
}
/** @see #getSignaturesFiles */
public void setSignaturesFiles(FileCollection signaturesFiles) {
data.signaturesFiles = signaturesFiles;
}
/**
* A list of references to URLs, which contain signatures and comments for forbidden API calls.
* The signatures are resolved against {@link #getClasspath()}.
* <p>
* This property is useful to refer to resources in plugin classpath, e.g., using
* {@link Class#getResource(String)}. It is not useful for general gradle builds. Especially,
* don't use it to refer to resources on foreign servers!
*/
@Input
@Optional
@Incubating
public Set<URL> getSignaturesURLs() {
return data.signaturesURLs;
}
/** @see #getSignaturesURLs */
public void setSignaturesURLs(Set<URL> signaturesURLs) {
data.signaturesURLs = signaturesURLs;
}
/**
* Gives multiple API signatures that are joined with newlines and
* parsed like a single {@link #getSignaturesFiles()}.
* The signatures are resolved against {@link #getClasspath()}.
*/
@Input
@Optional
public List<String> getSignatures() {
return data.signatures;
}
/** @see #getSignatures */
public void setSignatures(List<String> signatures) {
data.signatures = 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,...)
*/
@Input
@Optional
public Set<String> getBundledSignatures() {
return data.bundledSignatures;
}
/** @see #getBundledSignatures */
public void setBundledSignatures(Set<String> bundledSignatures) {
data.bundledSignatures = 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.
* Defaults to {@code false}.
* @deprecated Use <a href="bundled-signatures.html">bundled signatures</a> {@code "jdk-non-portable"} or {@code "jdk-internal"} instead.
*/
@Deprecated
@Input
public boolean getInternalRuntimeForbidden() {
return data.internalRuntimeForbidden;
}
/** @see #getInternalRuntimeForbidden
* @deprecated Use bundled signatures {@code "jdk-non-portable"} or {@code "jdk-internal"} instead.
*/
@Deprecated
public void setInternalRuntimeForbidden(boolean internalRuntimeForbidden) {
data.internalRuntimeForbidden = 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.
* Defaults to {@code false}.
*/
@Input
public boolean getFailOnUnsupportedJava() {
return data.failOnUnsupportedJava;
}
/** @see #getFailOnUnsupportedJava */
public void setFailOnUnsupportedJava(boolean failOnUnsupportedJava) {
data.failOnUnsupportedJava = 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 task
* (Gradle does this by default).
* Defaults to {@code true}.
*/
@Input
public boolean getFailOnMissingClasses() {
return data.failOnMissingClasses;
}
/** @see #getFailOnMissingClasses */
public void setFailOnMissingClasses(boolean failOnMissingClasses) {
data.failOnMissingClasses = 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.
* Defaults to {@code true}.
*/
@Input
public boolean getFailOnUnresolvableSignatures() {
return data.failOnUnresolvableSignatures;
}
/** @see #getFailOnUnresolvableSignatures */
public void setFailOnUnresolvableSignatures(boolean failOnUnresolvableSignatures) {
data.failOnUnresolvableSignatures = failOnUnresolvableSignatures;
}
/**
* {@inheritDoc}
* <p>
* This setting is to conform with {@link VerificationTask} interface.
* Other ForbiddenApis implementations use another name: {@code failOnViolation}
* Default is {@code false}.
*/
@Override
@Input
public boolean getIgnoreFailures() {
return data.ignoreFailures;
}
@Override
public void setIgnoreFailures(boolean ignoreFailures) {
data.ignoreFailures = ignoreFailures;
}
/**
* 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 plugin, 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.
* <p>
* The default is {@code false}, unless the plugin detects that your build is
* running in the <em>Gradle Daemon</em> (which has this problem), setting the
* default to {@code true} as a consequence.
* @since 2.2
*/
@Input
public boolean getDisableClassloadingCache() {
return data.disableClassloadingCache;
}
/** @see #getDisableClassloadingCache */
public void setDisableClassloadingCache(boolean disableClassloadingCache) {
data.disableClassloadingCache = disableClassloadingCache;
}
/**
* 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}).
*/
@Input
@Optional
public Set<String> getSuppressAnnotations() {
return data.suppressAnnotations;
}
/** @see #getSuppressAnnotations */
public void setSuppressAnnotations(Set<String> suppressAnnotations) {
data.suppressAnnotations = suppressAnnotations;
}
/**
* 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 task.
* Defaults to {@code project.targetCompatibility}.
*/
@Input
@Optional
public String getTargetCompatibility() {
return targetCompatibility;
}
/** @see #getTargetCompatibility */
public void setTargetCompatibility(String targetCompatibility) {
this.targetCompatibility = targetCompatibility;
}
// PatternFilterable implementation:
/**
* {@inheritDoc}
* <p>
* Set 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'
*/
@Override
@Input
public Set<String> getIncludes() {
return getPatternSet().getIncludes();
}
@Override
public CheckForbiddenApis setIncludes(Iterable<String> includes) {
getPatternSet().setIncludes(includes);
return this;
}
/**
* {@inheritDoc}
* <p>
* Set of patterns matching class files to be excluded from checking.
*/
@Override
@Input
public Set<String> getExcludes() {
return getPatternSet().getExcludes();
}
@Override
public CheckForbiddenApis setExcludes(Iterable<String> excludes) {
getPatternSet().setExcludes(excludes);
return this;
}
@Override
public CheckForbiddenApis exclude(String... arg0) {
getPatternSet().exclude(arg0);
return this;
}
@Override
public CheckForbiddenApis exclude(Iterable<String> arg0) {
getPatternSet().exclude(arg0);
return this;
}
@Override
public CheckForbiddenApis exclude(Spec<FileTreeElement> arg0) {
getPatternSet().exclude(arg0);
return this;
}
@Override
public CheckForbiddenApis exclude(@SuppressWarnings("rawtypes") Closure arg0) {
getPatternSet().exclude(arg0);
return this;
}
@Override
public CheckForbiddenApis include(String... arg0) {
getPatternSet().include(arg0);
return this;
}
@Override
public CheckForbiddenApis include(Iterable<String> arg0) {
getPatternSet().include(arg0);
return this;
}
@Override
public CheckForbiddenApis include(Spec<FileTreeElement> arg0) {
getPatternSet().include(arg0);
return this;
}
@Override
public CheckForbiddenApis include(@SuppressWarnings("rawtypes") Closure arg0) {
getPatternSet().include(arg0);
return this;
}
/** Returns the classes to check. */
@InputFiles
@SkipWhenEmpty
public FileTree getClassFiles() {
return getProject().files(getClassesDir()).getAsFileTree().matching(getPatternSet());
}
/** Executes the forbidden apis task. */
@TaskAction
public void checkForbidden() throws ForbiddenApiException {
final File classesDir = getClassesDir();
final FileCollection classpath = getClasspath();
if (classesDir == null || classpath == null) {
throw new InvalidUserDataException("Missing 'classesDir' or 'classpath' property.");
}
final Logger log = new Logger() {
@Override
public void error(String msg) {
getLogger().error(msg);
}
@Override
public void warn(String msg) {
getLogger().warn(msg);
}
@Override
public void info(String msg) {
getLogger().info(msg);
}
};
final Set<File> cpElements = classpath.getFiles();
final URL[] urls = new URL[cpElements.size() + 1];
try {
int i = 0;
for (final File cpElement : cpElements) {
urls[i++] = cpElement.toURI().toURL();
}
urls[i++] = classesDir.toURI().toURL();
assert i == urls.length;
} catch (MalformedURLException mfue) {
throw new InvalidUserDataException("Failed to build classpath URLs.", mfue);
}
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 (getFailOnMissingClasses()) options.add(FAIL_ON_MISSING_CLASSES);
if (!getIgnoreFailures()) options.add(FAIL_ON_VIOLATION);
if (getFailOnUnresolvableSignatures()) options.add(FAIL_ON_UNRESOLVABLE_SIGNATURES);
if (getDisableClassloadingCache()) 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 plugin. Please run the checks with a supported JDK!",
System.getProperty("java.runtime.name"), System.getProperty("java.runtime.version"));
if (getFailOnUnsupportedJava()) {
throw new GradleException(msg);
} else {
log.warn(msg);
return;
}
}
final Set<String> suppressAnnotations = getSuppressAnnotations();
if (suppressAnnotations != null) {
for (String a : suppressAnnotations) {
checker.addSuppressAnnotation(a);
}
}
try {
final Set<String> bundledSignatures = getBundledSignatures();
if (bundledSignatures != null) {
final String bundledSigsJavaVersion = getTargetCompatibility();
if (bundledSigsJavaVersion == null) {
log.warn("The 'targetCompatibility' project or task 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 : bundledSignatures) {
checker.addBundledSignatures(bs, bundledSigsJavaVersion);
}
}
if (getInternalRuntimeForbidden()) {
log.warn(DEPRECATED_WARN_INTERNALRUNTIME);
checker.addBundledSignatures(BS_JDK_NONPORTABLE, null);
}
final FileCollection signaturesFiles = getSignaturesFiles();
if (signaturesFiles != null) for (final File f : signaturesFiles) {
checker.parseSignaturesFile(f);
}
final Set<URL> signaturesURLs = getSignaturesURLs();
if (signaturesURLs != null) for (final URL url : signaturesURLs) {
checker.parseSignaturesFile(url);
}
final List<String> signatures = getSignatures();
if (signatures != null && !signatures.isEmpty()) {
final StringBuilder sb = new StringBuilder();
for (String line : signatures) {
sb.append(line).append(NL);
}
checker.parseSignaturesString(sb.toString());
}
} catch (IOException ioe) {
throw new ResourceException("IO problem while reading files with API signatures.", ioe);
} catch (ParseException pe) {
throw new InvalidUserDataException("Parsing signatures failed: " + pe.getMessage(), pe);
}
if (checker.hasNoSignatures()) {
if (options.contains(FAIL_ON_UNRESOLVABLE_SIGNATURES)) {
throw new InvalidUserDataException("No API signatures found; use properties 'signatures', 'bundledSignatures', 'signaturesURLs', and/or 'signaturesFiles' to define those!");
} else {
log.info("Skipping execution because no API signatures are available.");
return;
}
}
try {
checker.addClassesToCheck(getClassFiles());
} catch (IOException ioe) {
throw new ResourceException("Failed to load one of the given class files.", ioe);
}
checker.run();
} finally {
// Java 7 supports closing URLClassLoader, so check for Closeable interface:
if (urlLoader instanceof Closeable) try {
((Closeable) urlLoader).close();
} catch (IOException ioe) {
// ignore
}
}
}
}