package org.checkerframework.framework.type;

import java.io.Closeable;
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.URI;
import java.net.URISyntaxException;
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 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;
import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods;
import org.checkerframework.checker.mustcall.qual.InheritableMustCall;
import org.checkerframework.checker.mustcall.qual.Owning;
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.SystemUtil;
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.
 */
@SuppressWarnings(
    "mustcall:inconsistent.mustcall.subtype" // No need to check that AnnotationClassLoaders are
// closed. (Just one is created per type factory.)
)
@InheritableMustCall({})
public class AnnotationClassLoader implements Closeable {
  /** 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. */
  @SuppressWarnings("builder:required.method.not.called") // this class is @MustCall({})
  protected final @Owning 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(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(SystemUtil.dotSplitter.splitToList(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();
  }

  @EnsuresCalledMethods(value = "classLoader", methods = "close")
  @Override
  public void close() {
    try {
      classLoader.close();
    } catch (IOException e) {
      checker.message(Diagnostic.Kind.NOTE, "Failed to close AnnotationClassLoader");
    }
  }

  /**
   * 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 @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 boolean containsPackage(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();
        try (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 boolean checkJarForPackage(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 boolean checkDirForPackage(File currentDir, 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 @Nullable URL getDirectoryURL(String absolutePathToDirectory) {
    URL directoryURL = null;

    try {
      directoryURL = new File(absolutePathToDirectory).toURI().toURL();
    } catch (MalformedURLException e) {
      processingEnv
          .getMessager()
          .printMessage(
              Diagnostic.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 @Nullable URL getJarURL(String absolutePathToJarFile) {
    URL jarURL = null;

    try {
      jarURL = new URI("jar:file:" + absolutePathToJarFile + "!/").toURL();
    } catch (URISyntaxException | MalformedURLException e) {
      processingEnv
          .getMessager()
          .printMessage(Diagnostic.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 Set<String> getClasspaths() {
    Set<String> paths = new LinkedHashSet<>();

    // add all extension paths
    paths.addAll(SystemUtil.getPathsProperty("java.ext.dirs"));

    // add all paths in CLASSPATH, -cp, and -classpath
    paths.addAll(SystemUtil.getPathsProperty("java.class.path"));

    // 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 @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
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "bootclass path:");
    for (String path : SystemUtil.getPathsProperty("sun.boot.class.path")) {
      processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "\t" + path);
    }

    // all extension paths
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "extension dirs:");
    for (String path : SystemUtil.getPathsProperty("java.ext.dirs")) {
      processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "\t" + path);
    }

    // all paths in CLASSPATH, -cp, and -classpath
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "java.class.path property:");
    for (String path : SystemUtil.getPathsProperty("java.class.path")) {
      processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "\t" + path);
    }

    // add all paths that are examined by the classloader
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "classloader examined paths:");
    if (classLoader != null) {
      URL[] urls = classLoader.getURLs();
      for (int i = 0; i < urls.length; i++) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "\t" + urls[i].getFile());
      }
    } else {
      processingEnv.getMessager().printMessage(Diagnostic.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 Set<@BinaryName String> getBundledAnnotationNamesFromJar(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(
      @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(
      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 Set<@BinaryName String> getAnnotationNamesFromDirectory(
      @Nullable @DotSeparatedIdentifiers String packageName,
      File rootDirectory,
      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();
    if (directoryContents == null) {
      throw new UserError("Directory does not exist: %s", currentDirectory);
    }
    Arrays.sort(directoryContents, Comparator.comparing(File::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(
      @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(
      @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(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(Class<? extends Annotation> annoClass) {
    return true;
  }
}
