code: ///////////////////////////////////////////////////////////////////////////////////////////////
  // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
  // Copyright (C) 2001-2023 the original author or authors.
  //
  // This library is free software; you can redistribute it and/or
  // modify it under the terms of the GNU Lesser General Public
  // License as published by the Free Software Foundation; either
  // version 2.1 of the License, or (at your option) any later version.
  //
  // This library is distributed in the hope that it will be useful,
  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  // Lesser General Public License for more details.
  //
  // You should have received a copy of the GNU Lesser General Public
  // License along with this library; if not, write to the Free Software
  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  ///////////////////////////////////////////////////////////////////////////////////////////////
  
  package com.puppycrawl.tools.checkstyle;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.io.UnsupportedEncodingException;
  import java.nio.charset.Charset;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Locale;
  import java.util.Set;
  import java.util.SortedSet;
  import java.util.TreeSet;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  import org.apache.commons.logging.Log;
  import org.apache.commons.logging.LogFactory;
  
  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
  import com.puppycrawl.tools.checkstyle.api.AuditListener;
  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
  import com.puppycrawl.tools.checkstyle.api.Configuration;
  import com.puppycrawl.tools.checkstyle.api.Context;
  import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
  import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
  import com.puppycrawl.tools.checkstyle.api.FileText;
  import com.puppycrawl.tools.checkstyle.api.Filter;
  import com.puppycrawl.tools.checkstyle.api.FilterSet;
  import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
  import com.puppycrawl.tools.checkstyle.api.RootModule;
  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
  import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
  import com.puppycrawl.tools.checkstyle.api.Violation;
  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
  
  /**
   * This class provides the functionality to check a set of files.
   */
  public class Checker extends AbstractAutomaticBean implements MessageDispatcher, RootModule {
  
      /** Message to use when an exception occurs and should be printed as a violation. */
      public static final String EXCEPTION_MSG = "general.exception";
  
      /** Logger for Checker. */
      private final Log log;
  
      /** Maintains error count. */
      private final SeverityLevelCounter counter = new SeverityLevelCounter(
              SeverityLevel.ERROR);
  
      /** Vector of listeners. */
      private final List<AuditListener> listeners = new ArrayList<>();
  
      /** Vector of fileset checks. */
      private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
  
      /** The audit event before execution file filters. */
      private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
              new BeforeExecutionFileFilterSet();
  
      /** The audit event filters. */
      private final FilterSet filters = new FilterSet();
  
      /** The basedir to strip off in file names. */
      private String basedir;
  
      /** Locale country to report messages . **/
      @XdocsPropertyType(PropertyType.LOCALE_COUNTRY)
      private String localeCountry = Locale.getDefault().getCountry();
      /** Locale language to report messages . **/
      @XdocsPropertyType(PropertyType.LOCALE_LANGUAGE)
      private String localeLanguage = Locale.getDefault().getLanguage();
  
      /** The factory for instantiating submodules. */
      private ModuleFactory moduleFactory;
  
      /** The classloader used for loading Checkstyle module classes. */
      private ClassLoader moduleClassLoader;
  
      /** The context of all child components. */
      private Context childContext;
  
      /** The file extensions that are accepted. */
      private String[] fileExtensions = CommonUtil.EMPTY_STRING_ARRAY;
  
      /**
       * The severity level of any violations found by submodules.
       * The value of this property is passed to submodules via
       * contextualize().
       *
       * <p>Note: Since the Checker is merely a container for modules
       * it does not make sense to implement logging functionality
       * here. Consequently, Checker does not extend AbstractViolationReporter,
       * leading to a bit of duplicated code for severity level setting.
       */
      private SeverityLevel severity = SeverityLevel.ERROR;
  
      /** Name of a charset. */
      private String charset = StandardCharsets.UTF_8.name();
  
      /** Cache file. **/
      @XdocsPropertyType(PropertyType.FILE)
      private PropertyCacheFile cacheFile;
  
      /** Controls whether exceptions should halt execution or not. */
      private boolean haltOnException = true;
  
      /** The tab width for column reporting. */
      private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
  
      /**
       * Creates a new {@code Checker} instance.
       * The instance needs to be contextualized and configured.
       */
      public Checker() {
          addListener(counter);
          log = LogFactory.getLog(Checker.class);
      }
  
      /**
       * Sets cache file.
       *
       * @param fileName the cache file.
       * @throws IOException if there are some problems with file loading.
       */
      public void setCacheFile(String fileName) throws IOException {
          final Configuration configuration = getConfiguration();
          cacheFile = new PropertyCacheFile(configuration, fileName);
          cacheFile.load();
      }
  
      /**
       * Removes before execution file filter.
       *
       * @param filter before execution file filter to remove.
       */
      public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
          beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
      }
  
      /**
       * Removes filter.
       *
       * @param filter filter to remove.
       */
      public void removeFilter(Filter filter) {
          filters.removeFilter(filter);
      }
  
      @Override
      public void destroy() {
          listeners.clear();
          fileSetChecks.clear();
          beforeExecutionFileFilters.clear();
          filters.clear();
          if (cacheFile != null) {
              try {
                  cacheFile.persist();
              }
              catch (IOException ex) {
                  throw new IllegalStateException("Unable to persist cache file.", ex);
              }
          }
      }
  
      /**
       * Removes a given listener.
       *
       * @param listener a listener to remove
       */
      public void removeListener(AuditListener listener) {
          listeners.remove(listener);
      }
  
      /**
       * Sets base directory.
       *
       * @param basedir the base directory to strip off in file names
       */
      public void setBasedir(String basedir) {
          this.basedir = basedir;
      }
  
      @Override
      public int process(List<File> files) throws CheckstyleException {
          if (cacheFile != null) {
              cacheFile.putExternalResources(getExternalResourceLocations());
          }
  
          // Prepare to start
          fireAuditStarted();
          for (final FileSetCheck fsc : fileSetChecks) {
              fsc.beginProcessing(charset);
          }
  
          final List<File> targetFiles = files.stream()
                  .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
                  .collect(Collectors.toList());
          processFiles(targetFiles);
  
          // Finish up
          // It may also log!!!
          fileSetChecks.forEach(FileSetCheck::finishProcessing);
  
          // It may also log!!!
          fileSetChecks.forEach(FileSetCheck::destroy);
  
          final int errorCount = counter.getCount();
          fireAuditFinished();
          return errorCount;
      }
  
      /**
       * Returns a set of external configuration resource locations which are used by all file set
       * checks and filters.
       *
       * @return a set of external configuration resource locations which are used by all file set
       *         checks and filters.
       */
      private Set<String> getExternalResourceLocations() {
          return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream())
              .filter(ExternalResourceHolder.class::isInstance)
              .map(ExternalResourceHolder.class::cast)
              .flatMap(resource -> resource.getExternalResourceLocations().stream())
              .collect(Collectors.toSet());
      }
  
      /** Notify all listeners about the audit start. */
      private void fireAuditStarted() {
          final AuditEvent event = new AuditEvent(this);
          for (final AuditListener listener : listeners) {
              listener.auditStarted(event);
          }
      }
  
      /** Notify all listeners about the audit end. */
      private void fireAuditFinished() {
          final AuditEvent event = new AuditEvent(this);
          for (final AuditListener listener : listeners) {
              listener.auditFinished(event);
          }
      }
  
      /**
       * Processes a list of files with all FileSetChecks.
       *
       * @param files a list of files to process.
       * @throws CheckstyleException if error condition within Checkstyle occurs.
       * @throws Error wraps any java.lang.Error happened during execution
       * @noinspection ProhibitedExceptionThrown
       * @noinspectionreason ProhibitedExceptionThrown - There is no other way to
       *      deliver filename that was under processing.
       */
      // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
      private void processFiles(List<File> files) throws CheckstyleException {
          for (final File file : files) {
              String fileName = null;
              try {
                  fileName = file.getAbsolutePath();
                  final long timestamp = file.lastModified();
                  if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
                          || !acceptFileStarted(fileName)) {
                      continue;
                  }
                  if (cacheFile != null) {
                      cacheFile.put(fileName, timestamp);
                  }
                  fireFileStarted(fileName);
                  final SortedSet<Violation> fileMessages = processFile(file);
                  fireErrors(fileName, fileMessages);
                  fireFileFinished(fileName);
              }
              // -@cs[IllegalCatch] There is no other way to deliver filename that was under
              // processing. See https://github.com/checkstyle/checkstyle/issues/2285
              catch (Exception ex) {
                  if (fileName != null && cacheFile != null) {
                      cacheFile.remove(fileName);
                  }
  
                  // We need to catch all exceptions to put a reason failure (file name) in exception
                  throw new CheckstyleException("Exception was thrown while processing "
                          + file.getPath(), ex);
              }
              catch (Error error) {
                  if (fileName != null && cacheFile != null) {
                      cacheFile.remove(fileName);
                  }
  
                  // We need to catch all errors to put a reason failure (file name) in error
                  throw new Error("Error was thrown while processing " + file.getPath(), error);
              }
          }
      }
  
      /**
       * Processes a file with all FileSetChecks.
       *
       * @param file a file to process.
       * @return a sorted set of violations to be logged.
       * @throws CheckstyleException if error condition within Checkstyle occurs.
       * @noinspection ProhibitedExceptionThrown
       * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
       *      haltOnException field
       */
      private SortedSet<Violation> processFile(File file) throws CheckstyleException {
          final SortedSet<Violation> fileMessages = new TreeSet<>();
          try {
              final FileText theText = new FileText(file.getAbsoluteFile(), charset);
              for (final FileSetCheck fsc : fileSetChecks) {
                  fileMessages.addAll(fsc.process(file, theText));
              }
          }
          catch (final IOException ioe) {
              log.debug("IOException occurred.", ioe);
              fileMessages.add(new Violation(1,
                      Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
                      new String[] {ioe.getMessage()}, null, getClass(), null));
          }
          // -@cs[IllegalCatch] There is no other way to obey haltOnException field
          catch (Exception ex) {
              if (haltOnException) {
                  throw ex;
              }
  
              log.debug("Exception occurred.", ex);
  
              final StringWriter sw = new StringWriter();
              final PrintWriter pw = new PrintWriter(sw, true);
  
              ex.printStackTrace(pw);
  
              fileMessages.add(new Violation(1,
                      Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
                      new String[] {sw.getBuffer().toString()},
                      null, getClass(), null));
          }
          return fileMessages;
      }
  
      /**
       * Check if all before execution file filters accept starting the file.
       *
       * @param fileName
       *            the file to be audited
       * @return {@code true} if the file is accepted.
       */
      private boolean acceptFileStarted(String fileName) {
          final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
          return beforeExecutionFileFilters.accept(stripped);
      }
  
      /**
       * Notify all listeners about the beginning of a file audit.
       *
       * @param fileName
       *            the file to be audited
       */
      @Override
      public void fireFileStarted(String fileName) {
          final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
          final AuditEvent event = new AuditEvent(this, stripped);
          for (final AuditListener listener : listeners) {
              listener.fileStarted(event);
          }
      }
  
      /**
       * Notify all listeners about the errors in a file.
       *
       * @param fileName the audited file
       * @param errors the audit errors from the file
       */
      @Override
      public void fireErrors(String fileName, SortedSet<Violation> errors) {
          final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
          boolean hasNonFilteredViolations = false;
          for (final Violation element : errors) {
              final AuditEvent event = new AuditEvent(this, stripped, element);
              if (filters.accept(event)) {
                  hasNonFilteredViolations = true;
                  for (final AuditListener listener : listeners) {
                      listener.addError(event);
                  }
              }
          }
          if (hasNonFilteredViolations && cacheFile != null) {
              cacheFile.remove(fileName);
          }
      }
  
      /**
       * Notify all listeners about the end of a file audit.
       *
       * @param fileName
       *            the audited file
       */
      @Override
      public void fireFileFinished(String fileName) {
          final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
          final AuditEvent event = new AuditEvent(this, stripped);
          for (final AuditListener listener : listeners) {
              listener.fileFinished(event);
          }
      }
  
      @Override
      protected void finishLocalSetup() throws CheckstyleException {
          final Locale locale = new Locale(localeLanguage, localeCountry);
          LocalizedMessage.setLocale(locale);
  
          if (moduleFactory == null) {
              if (moduleClassLoader == null) {
                  throw new CheckstyleException(
                          "if no custom moduleFactory is set, "
                                  + "moduleClassLoader must be specified");
              }
  
              final Set<String> packageNames = PackageNamesLoader
                      .getPackageNames(moduleClassLoader);
              moduleFactory = new PackageObjectFactory(packageNames,
                      moduleClassLoader);
          }
  
          final DefaultContext context = new DefaultContext();
          context.add("charset", charset);
          context.add("moduleFactory", moduleFactory);
          context.add("severity", severity.getName());
          context.add("basedir", basedir);
          context.add("tabWidth", String.valueOf(tabWidth));
          childContext = context;
      }
  
      /**
       * {@inheritDoc} Creates child module.
       *
       * @noinspection ChainOfInstanceofChecks
       * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
       */
      @Override
      protected void setupChild(Configuration childConf)
              throws CheckstyleException {
          final String name = childConf.getName();
          final Object child;
  
          try {
              child = moduleFactory.createModule(name);
  
              if (child instanceof AbstractAutomaticBean) {
                  final AbstractAutomaticBean bean = (AbstractAutomaticBean) child;
                  bean.contextualize(childContext);
                  bean.configure(childConf);
              }
          }
          catch (final CheckstyleException ex) {
              throw new CheckstyleException("cannot initialize module " + name
                      + " - " + ex.getMessage(), ex);
          }
          if (child instanceof FileSetCheck) {
              final FileSetCheck fsc = (FileSetCheck) child;
              fsc.init();
              addFileSetCheck(fsc);
          }
          else if (child instanceof BeforeExecutionFileFilter) {
              final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
              addBeforeExecutionFileFilter(filter);
          }
          else if (child instanceof Filter) {
              final Filter filter = (Filter) child;
              addFilter(filter);
          }
          else if (child instanceof AuditListener) {
              final AuditListener listener = (AuditListener) child;
              addListener(listener);
          }
          else {
              throw new CheckstyleException(name
                      + " is not allowed as a child in Checker");
          }
      }
  
      /**
       * Adds a FileSetCheck to the list of FileSetChecks
       * that is executed in process().
       *
       * @param fileSetCheck the additional FileSetCheck
       */
      public void addFileSetCheck(FileSetCheck fileSetCheck) {
          fileSetCheck.setMessageDispatcher(this);
          fileSetChecks.add(fileSetCheck);
      }
  
      /**
       * Adds a before execution file filter to the end of the event chain.
       *
       * @param filter the additional filter
       */
      public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
          beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
      }
  
      /**
       * Adds a filter to the end of the audit event filter chain.
       *
       * @param filter the additional filter
       */
      public void addFilter(Filter filter) {
          filters.addFilter(filter);
      }
  
      @Override
      public final void addListener(AuditListener listener) {
          listeners.add(listener);
      }
  
      /**
       * Sets the file extensions that identify the files that pass the
       * filter of this FileSetCheck.
       *
       * @param extensions the set of file extensions. A missing
       *     initial '.' character of an extension is automatically added.
       */
      public final void setFileExtensions(String... extensions) {
          if (extensions == null) {
              fileExtensions = null;
          }
          else {
              fileExtensions = new String[extensions.length];
              for (int i = 0; i < extensions.length; i++) {
                  final String extension = extensions[i];
                  if (CommonUtil.startsWithChar(extension, '.')) {
                      fileExtensions[i] = extension;
                  }
                  else {
                      fileExtensions[i] = "." + extension;
                  }
              }
          }
      }
  
      /**
       * Sets the factory for creating submodules.
       *
       * @param moduleFactory the factory for creating FileSetChecks
       */
      public void setModuleFactory(ModuleFactory moduleFactory) {
          this.moduleFactory = moduleFactory;
      }
  
      /**
       * Sets locale country.
       *
       * @param localeCountry the country to report messages
       */
      public void setLocaleCountry(String localeCountry) {
          this.localeCountry = localeCountry;
      }
  
      /**
       * Sets locale language.
       *
       * @param localeLanguage the language to report messages
       */
      public void setLocaleLanguage(String localeLanguage) {
          this.localeLanguage = localeLanguage;
      }
  
      /**
       * Sets the severity level.  The string should be one of the names
       * defined in the {@code SeverityLevel} class.
       *
       * @param severity  The new severity level
       * @see SeverityLevel
       */
      public final void setSeverity(String severity) {
          this.severity = SeverityLevel.getInstance(severity);
      }
  
      @Override
      public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
          this.moduleClassLoader = moduleClassLoader;
      }
  
      /**
       * Sets a named charset.
       *
       * @param charset the name of a charset
       * @throws UnsupportedEncodingException if charset is unsupported.
       */
      public void setCharset(String charset)
              throws UnsupportedEncodingException {
          if (!Charset.isSupported(charset)) {
              final String message = "unsupported charset: '" + charset + "'";
              throw new UnsupportedEncodingException(message);
          }
          this.charset = charset;
      }
  
      /**
       * Sets the field haltOnException.
       *
       * @param haltOnException the new value.
       */
      public void setHaltOnException(boolean haltOnException) {
          this.haltOnException = haltOnException;
      }
  
      /**
       * Set the tab width to report audit events with.
       *
       * @param tabWidth an {@code int} value
       */
      public final void setTabWidth(int tabWidth) {
          this.tabWidth = tabWidth;
      }
  
      /**
       * Clears the cache.
       */
      public void clearCache() {
          if (cacheFile != null) {
              cacheFile.reset();
          }
      }
  
  }
  