blob: d1c41af7f327633a28a07e1a56210ccddcf8c771 [file] [log] [blame]
package org.checkerframework.framework.type;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
import org.checkerframework.checker.signature.qual.Identifier;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.InternalUtils;
import org.checkerframework.javacutil.UserError;
import org.plumelib.reflection.Signatures;
/**
* This class assists the {@link AnnotatedTypeFactory} by reflectively looking up the list of
* annotation class names in each checker's qual directory, and then loading and returning it as a
* set of annotation classes. It can also look up and load annotation classes from external
* directories that are passed as arguments to checkers that have extension capabilities such as the
* Subtyping Checker, Fenum Checker, and Units Checker.
*
* <p>To load annotations using this class, their directory structure and package structure must be
* identical.
*
* <p>Only annotation classes that have the {@link Target} meta-annotation with the value of {@link
* ElementType#TYPE_USE} (and optionally {@link ElementType#TYPE_PARAMETER}) are loaded. If it has
* other {@link ElementType} values, it won't be loaded. Other annotation classes must be manually
* listed in a checker's annotated type factory by overriding {@link
* AnnotatedTypeFactory#createSupportedTypeQualifiers()}.
*
* <p>Checker writers may wish to subclass this class if they wish to implement some custom rules to
* filter or process loaded annotation classes, by providing an override implementation of {@link
* #isSupportedAnnotationClass(Class)}. See {@code
* org.checkerframework.checker.units.UnitsAnnotationClassLoader} for an example.
*/
public class AnnotationClassLoader {
/** For issuing errors to the user. */
protected final BaseTypeChecker checker;
// For loading from a source package directory
/** The package name. */
private final @DotSeparatedIdentifiers String packageName;
/** The package name, with periods replaced by slashes. */
private final String packageNameWithSlashes;
/** The atomic package names (the package name split at dots). */
private final List<@Identifier String> fullyQualifiedPackageNameSegments;
/** The name of a Checker's qualifier package. */
private static final String QUAL_PACKAGE = "qual";
// For loading from a Jar file
/** The suffix for a .jar file. */
private static final String JAR_SUFFIX = ".jar";
/** The suffix for a .class file. */
private static final String CLASS_SUFFIX = ".class";
// Constants
/** The package separator. */
private static final char DOT = '.';
/** The path separator, in .jar files, binary names, etc. */
private static final char SLASH = '/';
/**
* Processing Env used to create an {@link AnnotationBuilder}, which is in turn used to build the
* annotation mirror from the loaded class.
*/
protected final ProcessingEnvironment processingEnv;
/** The resource URL of the qual directory of a checker class. */
private final URL resourceURL;
/** The class loader used to load annotation classes. */
protected final URLClassLoader classLoader;
/**
* The annotation classes bundled with a checker (located in its qual directory) that are deemed
* supported by the checker (non-alias annotations). Each checker can override {@link
* #isSupportedAnnotationClass(Class)} to determine whether an annotation is supported or not.
* Call {@link #getBundledAnnotationClasses()} to obtain a reference to the set of classes.
*/
private final Set<Class<? extends Annotation>> supportedBundledAnnotationClasses;
/** The package separator: ".". */
private static final Pattern DOT_LITERAL_PATTERN =
Pattern.compile(Character.toString(DOT), Pattern.LITERAL);
/**
* Constructor for loading annotations defined for a checker.
*
* @param checker a {@link BaseTypeChecker} or its subclass
*/
@SuppressWarnings("signature") // TODO: reduce use of string manipulation
public AnnotationClassLoader(final BaseTypeChecker checker) {
this.checker = checker;
processingEnv = checker.getProcessingEnvironment();
// package name must use dots, this is later prepended to annotation
// class names as we load the classes using the class loader
Package checkerPackage = checker.getClass().getPackage();
packageName =
checkerPackage != null && !checkerPackage.getName().isEmpty()
? checkerPackage.getName() + DOT + QUAL_PACKAGE
: QUAL_PACKAGE;
// the package name with dots replaced by slashes will be used to scan file directories
packageNameWithSlashes = packageName.replace(DOT, SLASH);
// Each component of the fully qualified package name will be used later to recursively descend
// from a root directory to see if the package exists in some particular root directory.
fullyQualifiedPackageNameSegments = new ArrayList<>();
// from the fully qualified package name, split it at every dot then add to the list
fullyQualifiedPackageNameSegments.addAll(Arrays.asList(DOT_LITERAL_PATTERN.split(packageName)));
classLoader = getClassLoader();
URL localResourceURL;
if (classLoader != null) {
// if the application classloader is accessible, then directly retrieve the resource URL of
// the qual package resource URLs must use slashes
localResourceURL = classLoader.getResource(packageNameWithSlashes);
// thread based application classloader, if needed in the future:
// resourceURL =
// Thread.currentThread().getContextClassLoader().getResource(packageNameWithSlashes);
} else {
// Signal failure to find resource
localResourceURL = null;
}
if (localResourceURL == null) {
// if the application classloader is not accessible (which means the checker class was loaded
// using the bootstrap classloader)
// or if the classloader didn't find the package,
// then scan the classpaths to find a jar or directory which contains the qual package and set
// the resource URL to that jar or qual directory
localResourceURL = getURLFromClasspaths();
}
resourceURL = localResourceURL;
supportedBundledAnnotationClasses = new LinkedHashSet<>();
loadBundledAnnotationClasses();
}
/**
* Scans all classpaths and returns the resource URL to the jar which contains the checker's qual
* package, or the qual package directory if it exists, or null if no jar or directory contains
* the package.
*
* @return a URL to the jar that contains the qual package, or to the qual package's directory, or
* null if no jar or directory contains the qual package
*/
private final @Nullable URL getURLFromClasspaths() {
// TODO: This method could probably be replaced with
// io.github.classgraph.ClassGraph#getClasspathURIs()
// Debug use, uncomment if needed to see all of the classpaths (boot
// classpath, extension classpath, and classpath)
// printPaths();
URL url = null;
// obtain all classpaths
Set<String> paths = getClasspaths();
// In checkers, there will be a resource URL for the qual directory. But when called in the
// framework (eg GeneralAnnotatedTypeFactory), there won't be a resourceURL since there isn't a
// qual directory.
// Each path from the set of classpaths will be checked to see if it contains the qual directory
// of a checker, if so, the first directory or jar that contains the package will be used as the
// source for loading classes from the qual package.
// If either a directory or a jar contains the package, resourceURL will be updated to refer to
// that source, otherwise resourceURL remains as null.
// If both a jar and a directory contain the qual package, then the order of the jar and the
// directory in the command line option(s) or environment variables will decide which one gets
// examined first.
for (String path : paths) {
// see if the current classpath segment is a jar or a directory
if (path.endsWith(JAR_SUFFIX)) {
// current classpath segment is a jar
url = getJarURL(path);
// see if the jar contains the package
if (url != null && containsPackage(url)) {
return url;
}
} else {
// current classpath segment is a directory
url = getDirectoryURL(path);
// see if the directory contains the package
if (url != null && containsPackage(url)) {
// append a slash if necessary
if (!path.endsWith(Character.toString(SLASH))) {
path += SLASH;
}
// update URL to the qual directory
url = getDirectoryURL(path + packageNameWithSlashes);
return url;
}
}
}
// if no jar or directory contains the qual package, then return null
return null;
}
/**
* Checks to see if the jar or directory referred by the URL contains the qual package of a
* specific checker.
*
* @param url a URL referring to either a jar or a directory
* @return true if the jar or the directory contains the qual package, false otherwise
*/
private final boolean containsPackage(final URL url) {
// see whether the resource URL has a protocol of jar or file
if (url.getProtocol().equals("jar")) {
// try to open up the jar file
try {
JarURLConnection connection = (JarURLConnection) url.openConnection();
JarFile jarFile = connection.getJarFile();
// check to see if the jar file contains the package
return checkJarForPackage(jarFile);
} catch (IOException e) {
// do nothing for missing or un-openable Jar files
}
} else if (url.getProtocol().equals("file")) {
// open up the directory
File rootDir = new File(url.getFile());
// check to see if the directory contains the package
return checkDirForPackage(rootDir, fullyQualifiedPackageNameSegments.iterator());
}
return false;
}
/**
* Checks to see if the jar file contains the qual package of a specific checker.
*
* @param jar a jar file
* @return true if the jar file contains the qual package, false otherwise
*/
@SuppressWarnings("JdkObsolete")
private final boolean checkJarForPackage(final JarFile jar) {
Enumeration<JarEntry> jarEntries = jar.entries();
// loop through the entries in the jar
while (jarEntries.hasMoreElements()) {
JarEntry je = jarEntries.nextElement();
// Each entry is the fully qualified path and file name to a particular artifact in the jar
// file (eg a class file).
// If the jar has the package, one of the entry's name will begin with the package name in
// slash notation.
String entryName = je.getName();
if (entryName.startsWith(packageNameWithSlashes + SLASH)) {
return true;
}
}
return false;
}
/**
* Checks to see if the current directory contains the qual package through recursion currentDir
* starts at the root directory (a directory passed in as part of the classpaths), the iterator
* goes through each segment of the fully qualified package name (each segment is separated by a
* dot).
*
* <p>Each step of the recursion checks to see if there's a subdirectory in the current directory
* that has a name matching the package name segment, if so, it recursively descends into that
* subdirectory to check the next package name segment
*
* <p>If there's no more segments left, then we've found the qual directory of interest
*
* <p>If we've checked every subdirectory and none of them match the current package name segment,
* then the qual directory of interest does not exist in the given root directory (at the
* beginning of recursion)
*
* @param currentDir current directory
* @param pkgNames an iterator which provides each segment of the fully qualified qual package
* name
* @return true if the qual package exists within the root directory, false otherwise
*/
private final boolean checkDirForPackage(final File currentDir, final Iterator<String> pkgNames) {
// if the iterator has no more package name segments, then we've found
// the qual directory of interest
if (!pkgNames.hasNext()) {
return true;
}
// if the file doesn't exist or it isn't a directory, return false
if (currentDir == null || !currentDir.isDirectory()) {
return false;
}
// if it isn't empty, dequeue one segment of the fully qualified package name
String currentPackageDirName = pkgNames.next();
// scan current directory to see if there's a sub-directory that has a
// matching name as the package name segment
for (File file : currentDir.listFiles()) {
if (file.isDirectory() && file.getName().equals(currentPackageDirName)) {
// if so, recursively descend and look at the next segment of
// the package name
return checkDirForPackage(file, pkgNames);
}
}
// if no sub-directory has a matching name, then that means there isn't
// a matching qual package
return false;
}
/**
* Given an absolute path to a directory, this method will return a URL reference to that
* directory.
*
* @param absolutePathToDirectory an absolute path to a directory
* @return a URL reference to the directory, or null if the URL is malformed
*/
private final @Nullable URL getDirectoryURL(final String absolutePathToDirectory) {
URL directoryURL = null;
try {
directoryURL = new File(absolutePathToDirectory).toURI().toURL();
} catch (MalformedURLException e) {
processingEnv
.getMessager()
.printMessage(Kind.NOTE, "Directory URL " + absolutePathToDirectory + " is malformed");
}
return directoryURL;
}
/**
* Given an absolute path to a jar file, this method will return a URL reference to that jar file.
*
* @param absolutePathToJarFile an absolute path to a jar file
* @return a URL reference to the jar file, or null if the URL is malformed
*/
private final @Nullable URL getJarURL(final String absolutePathToJarFile) {
URL jarURL = null;
try {
jarURL = new URL("jar:file:" + absolutePathToJarFile + "!/");
} catch (MalformedURLException e) {
processingEnv
.getMessager()
.printMessage(Kind.NOTE, "Jar URL " + absolutePathToJarFile + " is malformed");
}
return jarURL;
}
/**
* Obtains and returns a set of the classpaths from compiler options, system environment
* variables, and by examining the classloader to see what paths it has access to.
*
* <p>The classpaths will be obtained in the order of:
*
* <ol>
* <li>extension paths (from java.ext.dirs)
* <li>classpaths (set in {@code CLASSPATH}, or through {@code -classpath} and {@code -cp})
* <li>paths accessible and examined by the classloader
* </ol>
*
* In each of these paths, the order of the paths as specified in the command line options or
* environment variables will be the order returned in the set
*
* @return an immutable linked hashset of the classpaths
*/
private final Set<String> getClasspaths() {
Set<String> paths = new LinkedHashSet<>();
// add all extension paths
String extdirs = System.getProperty("java.ext.dirs");
if (extdirs != null && !extdirs.isEmpty()) {
paths.addAll(Arrays.asList(extdirs.split(File.pathSeparator)));
}
// add all paths in CLASSPATH, -cp, and -classpath
paths.addAll(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator)));
// add all paths that are examined by the classloader
if (classLoader != null) {
URL[] urls = classLoader.getURLs();
for (int i = 0; i < urls.length; i++) {
paths.add(urls[i].getFile().toString());
}
}
return Collections.unmodifiableSet(paths);
}
/**
* Obtains the classloader used to load the checker class, if that isn't available then it will
* try to obtain the system classloader.
*
* @return the classloader used to load the checker class, or the system classloader, or null if
* both are unavailable
*/
private final @Nullable URLClassLoader getClassLoader() {
ClassLoader result = InternalUtils.getClassLoaderForClass(checker.getClass());
if (result instanceof URLClassLoader) {
return (@Nullable URLClassLoader) result;
} else {
// Java 9+ use an internal classloader that doesn't support getting URLs. Ignore.
return null;
}
}
/** Debug Use: Displays all classpaths examined by the class loader. */
@SuppressWarnings("unused") // for debugging
protected final void printPaths() {
// all paths in Xbootclasspath
String[] bootclassPaths = System.getProperty("sun.boot.class.path").split(File.pathSeparator);
processingEnv.getMessager().printMessage(Kind.NOTE, "bootclass path:");
for (String path : bootclassPaths) {
processingEnv.getMessager().printMessage(Kind.NOTE, "\t" + path);
}
// all extension paths
String[] extensionDirs = System.getProperty("java.ext.dirs").split(File.pathSeparator);
processingEnv.getMessager().printMessage(Kind.NOTE, "extension dirs:");
for (String path : extensionDirs) {
processingEnv.getMessager().printMessage(Kind.NOTE, "\t" + path);
}
// all paths in CLASSPATH, -cp, and -classpath
processingEnv.getMessager().printMessage(Kind.NOTE, "java.class.path property:");
for (String path : System.getProperty("java.class.path").split(File.pathSeparator)) {
processingEnv.getMessager().printMessage(Kind.NOTE, "\t" + path);
}
// add all paths that are examined by the classloader
processingEnv.getMessager().printMessage(Kind.NOTE, "classloader examined paths:");
if (classLoader != null) {
URL[] urls = classLoader.getURLs();
for (int i = 0; i < urls.length; i++) {
processingEnv.getMessager().printMessage(Kind.NOTE, "\t" + urls[i].getFile());
}
} else {
processingEnv.getMessager().printMessage(Kind.NOTE, "classloader unavailable");
}
}
/**
* Loads the set of annotation classes in the qual directory of a checker shipped with the Checker
* Framework.
*/
private void loadBundledAnnotationClasses() {
// retrieve the fully qualified class names of the annotations
Set<@BinaryName String> annotationNames;
// see whether the resource URL has a protocol of jar or file
if (resourceURL != null && resourceURL.getProtocol().contentEquals("jar")) {
// if the checker class file is contained within a jar, then the resource URL for the qual
// directory will have the protocol "jar". This means the whole checker is loaded as a jar
// file.
JarURLConnection connection;
// create a connection to the jar file
try {
connection = (JarURLConnection) resourceURL.openConnection();
// disable caching / connection sharing of the low level URLConnection to the Jar file
connection.setDefaultUseCaches(false);
connection.setUseCaches(false);
// connect to the Jar file
connection.connect();
} catch (IOException e) {
throw new BugInCF(
"AnnotationClassLoader: cannot open a connection to the Jar file "
+ resourceURL.getFile());
}
// open up that jar file and extract annotation class names
try (JarFile jarFile = connection.getJarFile()) {
// get class names inside the jar file within the particular package
annotationNames = getBundledAnnotationNamesFromJar(jarFile);
} catch (IOException e) {
throw new BugInCF(
"AnnotationClassLoader: cannot open the Jar file " + resourceURL.getFile());
}
} else if (resourceURL != null && resourceURL.getProtocol().contentEquals("file")) {
// If the checker class file is found within the file system itself within some directory
// (usually development build directories), then process the package as a file directory in
// the file system and load the annotations contained in the qual directory.
// open up the directory
File packageDir = new File(resourceURL.getFile());
annotationNames = getAnnotationNamesFromDirectory(packageName, packageDir, packageDir);
} else {
// We do not support a resource URL with any other protocols, so create an empty set.
annotationNames = Collections.emptySet();
}
if (annotationNames.isEmpty()) {
PackageElement pkgEle = checker.getElementUtils().getPackageElement(packageName);
if (pkgEle != null) {
for (Element e : pkgEle.getEnclosedElements()) {
if (e.getKind() == ElementKind.ANNOTATION_TYPE) {
@SuppressWarnings("signature:assignment") // Elements needs to be annotated.
@BinaryName String annoBinName =
checker.getElementUtils().getBinaryName((TypeElement) e).toString();
annotationNames.add(annoBinName);
}
}
}
}
supportedBundledAnnotationClasses.addAll(loadAnnotationClasses(annotationNames));
}
/**
* Gets the set of annotation classes in the qual directory of a checker shipped with the Checker
* Framework. Note that the returned set from this method is mutable. This method is intended to
* be called within {@link AnnotatedTypeFactory#createSupportedTypeQualifiers()
* createSupportedTypeQualifiers()} (or its helper methods) to help define the set of supported
* qualifiers.
*
* @see AnnotatedTypeFactory#createSupportedTypeQualifiers()
* @return a mutable set of the loaded bundled annotation classes
*/
public final Set<Class<? extends Annotation>> getBundledAnnotationClasses() {
return supportedBundledAnnotationClasses;
}
/**
* Retrieves the annotation class file names from the qual directory contained inside a jar.
*
* @param jar the JarFile containing the annotation class files
* @return a set of fully qualified class names of the annotations
*/
@SuppressWarnings("JdkObsolete")
private final Set<@BinaryName String> getBundledAnnotationNamesFromJar(final JarFile jar) {
Set<@BinaryName String> annos = new LinkedHashSet<>();
// get an enumeration iterator for all the content entries in the jar file
Enumeration<JarEntry> jarEntries = jar.entries();
// enumerate through the entries
while (jarEntries.hasMoreElements()) {
JarEntry je = jarEntries.nextElement();
// filter out directories and non-class files
if (je.isDirectory() || !je.getName().endsWith(CLASS_SUFFIX)) {
continue;
}
String className = Signatures.classfilenameToBinaryName(je.getName());
// filter for qual package
if (className.startsWith(packageName + DOT)) {
// add to set
annos.add(className);
}
}
return annos;
}
/**
* This method takes as input the canonical name of an external annotation class and loads and
* returns that class via the class loader. This method returns null if the external annotation
* class was loaded successfully but was deemed not supported by a checker. Errors are issued if
* the external class is not an annotation, or if it could not be loaded successfully.
*
* @param annoName canonical name of an external annotation class, e.g.
* "myproject.qual.myannotation"
* @return the loaded annotation class, or null if it was not a supported annotation as decided by
* {@link #isSupportedAnnotationClass(Class)}
*/
public final @Nullable Class<? extends Annotation> loadExternalAnnotationClass(
final @BinaryName String annoName) {
return loadAnnotationClass(annoName, true);
}
/**
* This method takes as input a fully qualified path to a directory, and loads and returns the set
* of all supported annotation classes from that directory.
*
* @param dirName absolute path to a directory containing annotation classes
* @return a set of annotation classes
*/
public final Set<Class<? extends Annotation>> loadExternalAnnotationClassesFromDirectory(
final String dirName) {
File rootDirectory = new File(dirName);
Set<@BinaryName String> annoNames =
getAnnotationNamesFromDirectory(null, rootDirectory, rootDirectory);
return loadAnnotationClasses(annoNames);
}
/**
* Retrieves all annotation names from the current directory, and recursively descends and
* retrieves annotation names from sub-directories.
*
* @param packageName the name of the package that contains the qual package, or null
* @param rootDirectory a {@link File} object representing the root directory of a set of
* annotations, which is subtracted from class names to retrieve each class's fully qualified
* class names
* @param currentDirectory a {@link File} object representing the current sub-directory of the
* root directory
* @return a set fully qualified annotation class name, for annotations in the root directory or
* its sub-directories
*/
@SuppressWarnings("signature") // TODO: reduce use of string manipulation
private final Set<@BinaryName String> getAnnotationNamesFromDirectory(
final @Nullable @DotSeparatedIdentifiers String packageName,
final File rootDirectory,
final File currentDirectory) {
Set<@BinaryName String> results = new LinkedHashSet<>();
// Full path to root directory
String rootPath = rootDirectory.getAbsolutePath();
// check every file and directory within the current directory
File[] directoryContents = currentDirectory.listFiles();
Arrays.sort(
directoryContents,
new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.getName().compareTo(o2.getName());
}
});
for (File file : directoryContents) {
if (file.isFile()) {
// TODO: simplify all this string manipulation.
// Full file name, including path to file
String fullFileName = file.getAbsolutePath();
// Simple file name
String fileName =
fullFileName.substring(
fullFileName.lastIndexOf(File.separator) + 1, fullFileName.length());
// Path to file
String filePath = fullFileName.substring(0, fullFileName.lastIndexOf(File.separator));
// Package name beginning with "qual"
String qualPackage = null;
if (!filePath.equals(rootPath)) {
qualPackage =
filePath.substring(rootPath.length() + 1, filePath.length()).replace(SLASH, DOT);
}
// Simple annotation name, which is the same as the file name (without directory)
// but with file extension removed.
@BinaryName String annotationName = fileName;
if (fileName.lastIndexOf(DOT) != -1) {
annotationName = fileName.substring(0, fileName.lastIndexOf(DOT));
}
// Fully qualified annotation class name (a @BinaryName, not a @FullyQualifiedName)
@BinaryName String fullyQualifiedAnnoName =
Signatures.addPackage(packageName, Signatures.addPackage(qualPackage, annotationName));
if (fileName.endsWith(CLASS_SUFFIX)) {
// add the fully qualified annotation class name to the set
results.add(fullyQualifiedAnnoName);
}
} else if (file.isDirectory()) {
// recursively add all sub directories's fully qualified annotation class name
results.addAll(getAnnotationNamesFromDirectory(packageName, rootDirectory, file));
}
}
return results;
}
/**
* Loads the class indicated by the name, and checks to see if it is an annotation that is
* supported by a checker.
*
* @param className the name of the class, in binary name format
* @param issueError set to true to issue a warning when a loaded annotation is not a type
* annotation. It is useful to set this to true if a given annotation must be a well-defined
* type annotation (eg for annotation class names given as command line arguments). It should
* be set to false if the annotation is a meta-annotation or non-type annotation.
* @return the loaded annotation class if it has a {@code @Target} meta-annotation with the
* required ElementType values, and is a supported annotation by a checker. If the annotation
* is not supported by a checker, null is returned.
*/
protected final @Nullable Class<? extends Annotation> loadAnnotationClass(
final @BinaryName String className, boolean issueError) {
// load the class
Class<?> cls = null;
try {
if (classLoader != null) {
cls = Class.forName(className, true, classLoader);
} else {
cls = Class.forName(className);
}
} catch (ClassNotFoundException e) {
throw new UserError(
checker.getClass().getSimpleName()
+ ": could not load class for annotation: "
+ className
+ ". Ensure that it is a type annotation"
+ " and your classpath is correct.");
}
// If the freshly loaded class is not an annotation, then issue error if required and then
// return null
if (!cls.isAnnotation()) {
if (issueError) {
throw new UserError(
checker.getClass().getSimpleName()
+ ": the loaded class: "
+ cls.getCanonicalName()
+ " is not a type annotation.");
}
return null;
}
Class<? extends Annotation> annoClass = cls.asSubclass(Annotation.class);
// Check the loaded annotation to see if it has a @Target meta-annotation with the required
// ElementType values
if (hasWellDefinedTargetMetaAnnotation(annoClass)) {
// If so, return the loaded annotation if it is supported by a checker
return isSupportedAnnotationClass(annoClass) ? annoClass : null;
} else if (issueError) {
// issueError is set to true for loading explicitly named external annotations.
// We issue an error here when one of those annotations is not well-defined, since the
// user expects these external annotations to be loaded.
throw new UserError(
checker.getClass().getSimpleName()
+ ": the loaded annotation: "
+ annoClass.getCanonicalName()
+ " is not a type annotation."
+ " Check its @Target meta-annotation.");
} else {
// issueError is set to false for loading the qual directory or any external directories.
// We don't issue any errors since there may be meta-annotations or non-type annotations
// in such directories.
return null;
}
}
/**
* Loads a set of annotations indicated by their names.
*
* @param annoNames a set of binary names for annotation classes
* @return a set of loaded annotation classes
* @see #loadAnnotationClass(String, boolean)
*/
protected final Set<Class<? extends Annotation>> loadAnnotationClasses(
final @Nullable Set<@BinaryName String> annoNames) {
Set<Class<? extends Annotation>> loadedClasses = new LinkedHashSet<>();
if (annoNames != null && !annoNames.isEmpty()) {
// loop through each class name & load the class
for (String annoName : annoNames) {
Class<? extends Annotation> annoClass = loadAnnotationClass(annoName, false);
if (annoClass != null) {
loadedClasses.add(annoClass);
}
}
}
return loadedClasses;
}
/**
* Checks to see whether a particular annotation class has the {@link Target} meta-annotation, and
* has the required {@link ElementType} values.
*
* <p>A subclass may override this method to load annotations that are not intended to be
* annotated in source code. E.g.: {@code SubtypingChecker} overrides this method to load {@code
* Unqualified}.
*
* @param annoClass an annotation class
* @return true if the annotation is well defined, false if it isn't
*/
protected boolean hasWellDefinedTargetMetaAnnotation(
final Class<? extends Annotation> annoClass) {
return annoClass.getAnnotation(Target.class) != null
&& AnnotationUtils.hasTypeQualifierElementTypes(
annoClass.getAnnotation(Target.class).value(), annoClass);
}
/**
* Checks to see whether a particular annotation class is supported.
*
* <p>By default, all loaded annotations that pass the basic checks in {@link
* #loadAnnotationClass(String, boolean)} are supported.
*
* <p>Individual checkers can create a subclass of AnnotationClassLoader and override this method
* to indicate whether a particular annotation is supported.
*
* @param annoClass an annotation class
* @return true if the annotation is supported, false if it isn't
*/
protected boolean isSupportedAnnotationClass(final Class<? extends Annotation> annoClass) {
return true;
}
}