class LinkageCheckerRule extends AbstractNonCacheableEnforcerRule {

  @Override
  public void execute(@Nonnull EnforcerRuleHelper helper) throws EnforcerRuleException {
    logger = helper.getLog();

    try {
      MavenProject project = (MavenProject) helper.evaluate("${project}");
      MavenSession session = (MavenSession) helper.evaluate("${session}");
      MojoExecution execution = (MojoExecution) helper.evaluate("${mojoExecution}");
      RepositorySystemSession repositorySystemSession = session.getRepositorySession();

      ImmutableList<String> repositoryUrls =
          project.getRemoteProjectRepositories().stream()
              .map(RemoteRepository::getUrl)
              .collect(toImmutableList());
      DependencyGraphBuilder dependencyGraphBuilder = new DependencyGraphBuilder(repositoryUrls);
      classPathBuilder = new ClassPathBuilder(dependencyGraphBuilder);

      boolean readingDependencyManagementSection =
          dependencySection == DependencySection.DEPENDENCY_MANAGEMENT;
      if (readingDependencyManagementSection
          && (project.getDependencyManagement() == null
              || project.getDependencyManagement().getDependencies() == null
              || project.getDependencyManagement().getDependencies().isEmpty())) {
        logger.warn("The rule is set to read dependency management section but it is empty.");
      }

      String projectType = project.getArtifact().getType();
      if (readingDependencyManagementSection) {
        if (!"pom".equals(projectType)) {
          logger.warn("A BOM should have packaging pom");
          return;
        }
      } else {
        if (UNSUPPORTED_NONBOM_PACKAGING.contains(projectType)) {
          return;
        }
        if (!"verify".equals(execution.getLifecyclePhase())) {
          throw new EnforcerRuleException(
              "To run the check on the compiled class files, the linkage checker enforcer rule"
                  + " should be bound to the 'verify' phase. Current phase: "
                  + execution.getLifecyclePhase());
        }
        if (project.getArtifact().getFile() == null) {
          // Skipping projects without a file, such as Guava's guava-tests module.
          // https://github.com/GoogleCloudPlatform/cloud-opensource-java/issues/850
          return;
        }
      }

      ClassPathResult classPathResult =
          readingDependencyManagementSection
              ? findBomClasspath(project, repositorySystemSession)
              : findProjectClasspath(project, repositorySystemSession, helper);
      ImmutableList<ClassPathEntry> classPath = classPathResult.getClassPath();
      if (classPath.isEmpty()) {
        logger.warn("Class path is empty.");
        return;
      }

      // As sorted by level order, the first elements in classpath are the project and its direct
      // non-test dependencies.
      List<org.apache.maven.model.Dependency> dependencies = project.getDependencies();
      long projectDependencyCount =
          dependencies.stream().filter(dependency -> !"test".equals(dependency.getScope())).count();
      List<ClassPathEntry> entryPoints = classPath.subList(0, (int) projectDependencyCount + 1);

      try {

        // TODO LinkageChecker.create and LinkageChecker.findLinkageProblems
        // should not be two separate public methods since we always call
        // findLinkageProblems immediately after create.

        Path exclusionFile = this.exclusionFile == null ? null : Paths.get(this.exclusionFile);
        LinkageChecker linkageChecker =
            LinkageChecker.create(classPath, entryPoints, exclusionFile);
        ImmutableSet<LinkageProblem> linkageProblems = linkageChecker.findLinkageProblems();
        if (reportOnlyReachable) {
          ClassReferenceGraph classReferenceGraph = linkageChecker.getClassReferenceGraph();
          linkageProblems =
              linkageProblems.stream()
                  .filter(
                      entry ->
                          classReferenceGraph.isReachable(entry.getSourceClass().getBinaryName()))
                  .collect(toImmutableSet());
        }

        if (classPathResult != null) {
          LinkageProblemCauseAnnotator.annotate(classPathBuilder, classPathResult, linkageProblems);
        }

        // Count unique LinkageProblems by their symbols
        long errorCount =
            linkageProblems.stream().map(LinkageProblem::formatSymbolProblem).distinct().count();

        String foundError = reportOnlyReachable ? "reachable error" : "error";
        if (errorCount > 1) {
          foundError += "s";
        }
        if (errorCount > 0) {
          String message =
              String.format(
                  "Linkage Checker rule found %d %s:\n%s",
                  errorCount,
                  foundError,
                  LinkageProblem.formatLinkageProblems(linkageProblems, classPathResult));
          if (getLevel() == WARN) {
            logger.warn(message);
          } else {
            logger.error(message);
            logger.info(
                "For the details of the linkage errors, see "
                    + "https://github.com/GoogleCloudPlatform/cloud-opensource-java/wiki/Linkage-Checker-Messages");
            throw new EnforcerRuleException(
                "Failed while checking class path. See above error report.");
          }
        } else {
          // arguably shouldn't log anything on success
          logger.info("No " + foundError + " found");
        }
      } catch (IOException ex) {
        // Maven's "-e" flag does not work for EnforcerRuleException. Print stack trace here.
        logger.warn("Failed to run Linkage Checker", ex);
        return; // Not failing the build.
      }
    } catch (ExpressionEvaluationException ex) {
      throw new EnforcerRuleException("Unable to lookup an expression " + ex.getMessage(), ex);
    }
  }

}