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.tools.Diagnostic.Kind;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.checker.signature.qual.ClassGetName;
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;

    /**
     * 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(
                        Pattern.compile(Character.toString(DOT), Pattern.LITERAL)
                                .split(packageName)));

        classLoader = getClassLoader();

        if (classLoader != null) {
            // if the application classloader is accessible, then directly
            // retrieve the resource URL of the qual package
            // resource URLs must use slashes
            resourceURL = classLoader.getResource(packageNameWithSlashes);

            // thread based application classloader, if needed in the future:
            // resourceURL =
            // Thread.currentThread().getContextClassLoader().getResource(packageNameWithSlashes);
        } else {
            // if the application classloader is not accessible (which means the
            // checker class was loaded using the bootstrap classloader)
            // 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
            resourceURL = getURLFromClasspaths();
        }

        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() {
        // 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
     */
    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
        String[] javaclassPaths = System.getProperty("java.class.path").split(File.pathSeparator);
        processingEnv.getMessager().printMessage(Kind.NOTE, "java classpaths:");
        for (String path : javaclassPaths) {
            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() {
        // if there's no resourceURL, then there's nothing we can load
        if (resourceURL == null) {
            return;
        }

        // 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.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.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();
        }

        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
     */
    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
                @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 fullyQualifiedClassName, and checks to see if it is an
     * annotation that is supported by a checker.
     *
     * @param fullyQualifiedClassName the fully qualified name of the class
     * @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 @ClassGetName String fullyQualifiedClassName, boolean issueError) {

        // load the class
        Class<?> cls = null;
        try {
            if (classLoader != null) {
                cls = Class.forName(fullyQualifiedClassName, true, classLoader);
            } else {
                cls = Class.forName(fullyQualifiedClassName);
            }
        } catch (ClassNotFoundException e) {
            throw new UserError(
                    checker.getClass().getSimpleName()
                            + ": could not load class for annotation: "
                            + fullyQualifiedClassName
                            + ". 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 fullyQualifiedAnnoNames.
     *
     * @param fullyQualifiedAnnoNames a set of strings where each string is a single annotation
     *     class's fully qualified name
     * @return a set of loaded annotation classes
     * @see #loadAnnotationClass(String, boolean)
     */
    protected final Set<Class<? extends Annotation>> loadAnnotationClasses(
            final @Nullable Set<@BinaryName String> fullyQualifiedAnnoNames) {
        Set<Class<? extends Annotation>> loadedClasses = new LinkedHashSet<>();

        if (fullyQualifiedAnnoNames != null && !fullyQualifiedAnnoNames.isEmpty()) {
            // loop through each class name & load the class
            for (String fullyQualifiedAnnoName : fullyQualifiedAnnoNames) {
                Class<? extends Annotation> annoClass =
                        loadAnnotationClass(fullyQualifiedAnnoName, 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;
    }
}
