/* * (C) Copyright Uwe Schindler (Generics Policeman) and others. * Parts of this work are licensed to the Apache Software Foundation (ASF) * under one or more contributor license agreements. * * 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.ant; import static de.thetaphi.forbiddenapis.Checker.Option.*; import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.ProjectComponent; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.FileList; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Reference; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.types.resources.StringResource; import org.apache.tools.ant.types.resources.Union; 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.IOException; import java.io.File; import java.util.Collection; import java.util.EnumSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Locale; /** * Task to check if a set of class files contains calls to forbidden APIs * from a given classpath and list of API signatures (either inline or as pointer to files). * In contrast to other ANT tasks, this tool does only visit the given classpath * and the system classloader, not ANT's class loader. */ public class AntTask extends Task implements Constants { private final Union classFiles = new Union(); private final Union apiSignatures = new Union(); private final Collection<BundledSignaturesType> bundledSignatures = new LinkedHashSet<BundledSignaturesType>(); private final Collection<SuppressAnnotationType> suppressAnnotations = new LinkedHashSet<SuppressAnnotationType>(); private Path classpath = null; private boolean failOnUnsupportedJava = false; @Deprecated private boolean internalRuntimeForbidden = false; private boolean restrictClassFilename = true; private boolean failOnMissingClasses = true; private boolean failOnUnresolvableSignatures = true; private boolean failOnViolation = true; private boolean ignoreEmptyFileset = false; private String targetVersion = null; private boolean disableClassloadingCache = false; @Override public void execute() throws BuildException { final Logger log = new Logger() { @Override public void error(String msg) { log(msg, Project.MSG_ERR); } @Override public void warn(String msg) { // ANT has no real log levels printed, so prefix with "WARNING": log("WARNING: " + msg, Project.MSG_WARN); } @Override public void info(String msg) { log(msg, Project.MSG_INFO); } }; AntClassLoader antLoader = null; try { final ClassLoader loader; if (classpath != null) { classpath.setProject(getProject()); loader = antLoader = getProject().createClassLoader(ClassLoader.getSystemClassLoader(), classpath); antLoader.setParentFirst(true); // use default classloader delegation } else { loader = ClassLoader.getSystemClassLoader(); } classFiles.setProject(getProject()); apiSignatures.setProject(getProject()); 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 <%s/>. Please run the checks with a supported JDK!", System.getProperty("java.runtime.name"), System.getProperty("java.runtime.version"), getTaskName()); if (failOnUnsupportedJava) { throw new BuildException(msg); } else { log.warn(msg); return; } } for (final SuppressAnnotationType a : suppressAnnotations) { checker.addSuppressAnnotation(a.getClassname()); } try { for (BundledSignaturesType bs : bundledSignatures) { final String name = bs.getName(); if (name == null) { throw new BuildException("<bundledSignatures/> must have the mandatory attribute 'name' referring to a bundled signatures file."); } String targetVersion = this.targetVersion; if (bs.getTargetVersion() != null) { if (!name.startsWith("jdk-")) { throw new ParseException("Cannot supply a targetVersion for non-JDK signatures."); } targetVersion = bs.getTargetVersion(); } if (targetVersion == null && name.startsWith("jdk-")) { log.warn("The 'targetVersion' parameter is missing. " + "Trying to read bundled JDK signatures without compiler target. " + "You have to explicitely specify the version in the resource name."); } checker.addBundledSignatures(name, targetVersion); } if (internalRuntimeForbidden) { log.warn(DEPRECATED_WARN_INTERNALRUNTIME); checker.addBundledSignatures(BS_JDK_NONPORTABLE, null); } @SuppressWarnings("unchecked") final Iterator<Resource> iter = apiSignatures.iterator(); while (iter.hasNext()) { final Resource r = iter.next(); if (r instanceof StringResource) { final String s = ((StringResource) r).getValue(); if (s != null && s.trim().length() > 0) { checker.parseSignaturesString(s); } } else { checker.parseSignaturesFile(r.getInputStream(), r.toString()); } } } catch (IOException ioe) { throw new BuildException("IO problem while reading files with API signatures.", ioe); } catch (ParseException pe) { throw new BuildException("Parsing signatures failed: " + pe.getMessage(), pe); } if (checker.hasNoSignatures()) { throw new BuildException("No API signatures found; use signaturesFile=, <signatures*/>, <bundledSignatures/> or inner text to define those!"); } log.info("Loading classes to check..."); try { @SuppressWarnings("unchecked") final Iterator<Resource> iter = classFiles.iterator(); boolean foundClass = false; while (iter.hasNext()) { final Resource r = iter.next(); final String name = r.getName(); if (restrictClassFilename && name != null && !name.endsWith(".class")) { continue; } checker.addClassToCheck(r.getInputStream()); foundClass = true; } if (!foundClass) { if (ignoreEmptyFileset) { log.warn("There is no <fileset/> or other resource collection given, or the collection does not contain any class files to check."); log.info("Scanned 0 class files."); return; } else { throw new BuildException("There is no <fileset/> or other resource collection given, or the collection does not contain any class files to check."); } } } catch (IOException ioe) { throw new BuildException("Failed to load one of the given class files.", ioe); } try { checker.run(); } catch (ForbiddenApiException fae) { throw new BuildException(fae.getMessage(), fae.getCause()); } } finally { if (antLoader != null) antLoader.cleanup(); } } /** Set of class files to check */ public void add(ResourceCollection rc) { classFiles.add(rc); } /** Sets a directory as base for class files. The implicit pattern '**/*.class' is used to only scan class files. */ public void setDir(File dir) { final FileSet fs = new FileSet(); fs.setProject(getProject()); fs.setDir(dir); // needed if somebody sets restrictClassFilename=false: fs.setIncludes("**/*.class"); classFiles.add(fs); } private <T extends ProjectComponent & ResourceCollection> T addSignaturesResource(T res) { res.setProject(getProject()); apiSignatures.add(res); return res; } /** Set of files with API signatures as <signaturesFileSet/> nested element */ public FileSet createSignaturesFileSet() { return addSignaturesResource(new FileSet()); } /** List of files with API signatures as <signaturesFileList/> nested element */ public FileList createSignaturesFileList() { return addSignaturesResource(new FileList()); } /** Single file with API signatures as <signaturesFile/> nested element */ public FileResource createSignaturesFile() { return addSignaturesResource(new FileResource()); } /** Collection of arbitrary Ant resources or {@code <bundled/>} elements. */ public SignaturesResources createSignatures() { return addSignaturesResource(new SignaturesResources(this)); } /** A file with API signatures signaturesFile= attribute */ public void setSignaturesFile(File file) { createSignaturesFile().setFile(file); } /** Support for API signatures list as nested text */ public void addText(String text) { addSignaturesResource(new StringResource(text)); } /** Creates a bundled signatures instance */ public BundledSignaturesType createBundledSignatures() { final BundledSignaturesType s = new BundledSignaturesType(); s.setProject(getProject()); bundledSignatures.add(s); return s; } /** A bundled signatures name */ public void setBundledSignatures(String name) { createBundledSignatures().setName(name); } /** Creates a instance of an annotation class name that suppresses error reporting in classes/methods/fields. */ public SuppressAnnotationType createSuppressAnnotation() { final SuppressAnnotationType s = new SuppressAnnotationType(); s.setProject(getProject()); suppressAnnotations.add(s); return s; } /** Class name of annotation that suppresses error reporting in classes/methods/fields. */ public void setSuppressAnnotation(String classname) { createSuppressAnnotation().setClassname(classname); } /** Classpath as classpath= attribute */ public void setClasspath(Path classpath) { createClasspath().append(classpath); } /** Classpath as classpathRef= attribute */ public void setClasspathRef(Reference r) { createClasspath().setRefid(r); } /** Classpath as <classpath/> nested element */ public Path createClasspath() { if (this.classpath == null) { this.classpath = new Path(getProject()); } return this.classpath.createPath(); } /** * 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}. */ public void setFailOnUnsupportedJava(boolean failOnUnsupportedJava) { this.failOnUnsupportedJava = failOnUnsupportedJava; } /** * Fail the build, if a referenced class is missing. This requires * that you pass the whole classpath including all dependencies. * If you don't have all classes in the filesets, the application classes * must be reachable through this classpath, too. * Defaults to {@code true}. */ public void setFailOnMissingClasses(boolean failOnMissingClasses) { this.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. * Defaults to {@code true}. */ public void setFailOnUnresolvableSignatures(boolean failOnUnresolvableSignatures) { this.failOnUnresolvableSignatures = failOnUnresolvableSignatures; } /** * 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 bundled signatures {@code "jdk-non-portable"} or {@code "jdk-internal"} instead. */ @Deprecated public void setInternalRuntimeForbidden(boolean internalRuntimeForbidden) { this.internalRuntimeForbidden = internalRuntimeForbidden; } /** Automatically restrict resource names included to files with a name ending in '.class'. * This makes filesets easier, as the includes="**/*.class" is not needed. * Defaults to {@code true}. */ public void setRestrictClassFilename(boolean restrictClassFilename) { this.restrictClassFilename = restrictClassFilename; } /** Ignore empty fileset/resource collection and print a warning instead. * Defaults to {@code false}. */ public void setIgnoreEmptyFileSet(boolean ignoreEmptyFileset) { this.ignoreEmptyFileset = ignoreEmptyFileset; } /** * Fail the build if violations have been found. If this parameter is set to {@code false}, * then the build will continue even if violations have been found. * Defaults to {@code true}. */ public void setFailOnViolation(boolean failOnViolation) { this.failOnViolation = failOnViolation; } /** * 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 null}. * @since 2.1 */ public void setTargetVersion(String targetVersion) { this.targetVersion = targetVersion; } /** * 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 tasks, 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. * The default is {@code false}. * @since 2.2 */ public void setDisableClassloadingCache(boolean disableClassloadingCache) { this.disableClassloadingCache = disableClassloadingCache; } }