1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.io.StringWriter;
26  import java.io.UnsupportedEncodingException;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.util.ArrayList;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Set;
34  import java.util.SortedSet;
35  import java.util.TreeSet;
36  import java.util.stream.Collectors;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  
41  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
42  import com.puppycrawl.tools.checkstyle.api.AuditListener;
43  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
44  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
45  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
46  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
47  import com.puppycrawl.tools.checkstyle.api.Configuration;
48  import com.puppycrawl.tools.checkstyle.api.Context;
49  import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
50  import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
51  import com.puppycrawl.tools.checkstyle.api.FileText;
52  import com.puppycrawl.tools.checkstyle.api.Filter;
53  import com.puppycrawl.tools.checkstyle.api.FilterSet;
54  import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
55  import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
56  import com.puppycrawl.tools.checkstyle.api.RootModule;
57  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
58  import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
59  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
60  
61  /**
62   * This class provides the functionality to check a set of files.
63   */
64  public class Checker extends AutomaticBean implements MessageDispatcher, RootModule {
65  
66      /** Message to use when an exception occurs and should be printed as a violation. */
67      public static final String EXCEPTION_MSG = "general.exception";
68  
69      /** Logger for Checker. */
70      private final Log log;
71  
72      /** Maintains error count. */
73      private final SeverityLevelCounter counter = new SeverityLevelCounter(
74              SeverityLevel.ERROR);
75  
76      /** Vector of listeners. */
77      private final List<AuditListener> listeners = new ArrayList<>();
78  
79      /** Vector of fileset checks. */
80      private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
81  
82      /** The audit event before execution file filters. */
83      private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
84              new BeforeExecutionFileFilterSet();
85  
86      /** The audit event filters. */
87      private final FilterSet filters = new FilterSet();
88  
89      /** Class loader to resolve classes with. **/
90      private ClassLoader classLoader = Thread.currentThread()
91              .getContextClassLoader();
92  
93      /** The basedir to strip off in file names. */
94      private String basedir;
95  
96      /** Locale country to report messages . **/
97      private String localeCountry = Locale.getDefault().getCountry();
98      /** Locale language to report messages . **/
99      private String localeLanguage = Locale.getDefault().getLanguage();
100 
101     /** The factory for instantiating submodules. */
102     private ModuleFactory moduleFactory;
103 
104     /** The classloader used for loading Checkstyle module classes. */
105     private ClassLoader moduleClassLoader;
106 
107     /** The context of all child components. */
108     private Context childContext;
109 
110     /** The file extensions that are accepted. */
111     private String[] fileExtensions = CommonUtil.EMPTY_STRING_ARRAY;
112 
113     /**
114      * The severity level of any violations found by submodules.
115      * The value of this property is passed to submodules via
116      * contextualize().
117      *
118      * <p>Note: Since the Checker is merely a container for modules
119      * it does not make sense to implement logging functionality
120      * here. Consequently Checker does not extend AbstractViolationReporter,
121      * leading to a bit of duplicated code for severity level setting.
122      */
123     private SeverityLevel severity = SeverityLevel.ERROR;
124 
125     /** Name of a charset. */
126     private String charset = System.getProperty("file.encoding", StandardCharsets.UTF_8.name());
127 
128     /** Cache file. **/
129     private PropertyCacheFile cacheFile;
130 
131     /** Controls whether exceptions should halt execution or not. */
132     private boolean haltOnException = true;
133 
134     /** The tab width for column reporting. */
135     private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
136 
137     /**
138      * Creates a new {@code Checker} instance.
139      * The instance needs to be contextualized and configured.
140      */
141     public Checker() {
142         addListener(counter);
143         log = LogFactory.getLog(Checker.class);
144     }
145 
146     /**
147      * Sets cache file.
148      * @param fileName the cache file.
149      * @throws IOException if there are some problems with file loading.
150      */
151     public void setCacheFile(String fileName) throws IOException {
152         final Configuration configuration = getConfiguration();
153         cacheFile = new PropertyCacheFile(configuration, fileName);
154         cacheFile.load();
155     }
156 
157     /**
158      * Removes before execution file filter.
159      * @param filter before execution file filter to remove.
160      */
161     public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
162         beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
163     }
164 
165     /**
166      * Removes filter.
167      * @param filter filter to remove.
168      */
169     public void removeFilter(Filter filter) {
170         filters.removeFilter(filter);
171     }
172 
173     @Override
174     public void destroy() {
175         listeners.clear();
176         fileSetChecks.clear();
177         beforeExecutionFileFilters.clear();
178         filters.clear();
179         if (cacheFile != null) {
180             try {
181                 cacheFile.persist();
182             }
183             catch (IOException ex) {
184                 throw new IllegalStateException("Unable to persist cache file.", ex);
185             }
186         }
187     }
188 
189     /**
190      * Removes a given listener.
191      * @param listener a listener to remove
192      */
193     public void removeListener(AuditListener listener) {
194         listeners.remove(listener);
195     }
196 
197     /**
198      * Sets base directory.
199      * @param basedir the base directory to strip off in file names
200      */
201     public void setBasedir(String basedir) {
202         this.basedir = basedir;
203     }
204 
205     @Override
206     public int process(List<File> files) throws CheckstyleException {
207         if (cacheFile != null) {
208             cacheFile.putExternalResources(getExternalResourceLocations());
209         }
210 
211         // Prepare to start
212         fireAuditStarted();
213         for (final FileSetCheck fsc : fileSetChecks) {
214             fsc.beginProcessing(charset);
215         }
216 
217         final List<File> targetFiles = files.stream()
218                 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
219                 .collect(Collectors.toList());
220         processFiles(targetFiles);
221 
222         // Finish up
223         // It may also log!!!
224         fileSetChecks.forEach(FileSetCheck::finishProcessing);
225 
226         // It may also log!!!
227         fileSetChecks.forEach(FileSetCheck::destroy);
228 
229         final int errorCount = counter.getCount();
230         fireAuditFinished();
231         return errorCount;
232     }
233 
234     /**
235      * Returns a set of external configuration resource locations which are used by all file set
236      * checks and filters.
237      * @return a set of external configuration resource locations which are used by all file set
238      *         checks and filters.
239      */
240     private Set<String> getExternalResourceLocations() {
241         final Set<String> externalResources = new HashSet<>();
242         fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder)
243             .forEach(check -> {
244                 final Set<String> locations =
245                     ((ExternalResourceHolder) check).getExternalResourceLocations();
246                 externalResources.addAll(locations);
247             });
248         filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder)
249             .forEach(filter -> {
250                 final Set<String> locations =
251                     ((ExternalResourceHolder) filter).getExternalResourceLocations();
252                 externalResources.addAll(locations);
253             });
254         return externalResources;
255     }
256 
257     /** Notify all listeners about the audit start. */
258     private void fireAuditStarted() {
259         final AuditEvent event = new AuditEvent(this);
260         for (final AuditListener listener : listeners) {
261             listener.auditStarted(event);
262         }
263     }
264 
265     /** Notify all listeners about the audit end. */
266     private void fireAuditFinished() {
267         final AuditEvent event = new AuditEvent(this);
268         for (final AuditListener listener : listeners) {
269             listener.auditFinished(event);
270         }
271     }
272 
273     /**
274      * Processes a list of files with all FileSetChecks.
275      * @param files a list of files to process.
276      * @throws CheckstyleException if error condition within Checkstyle occurs.
277      * @noinspection ProhibitedExceptionThrown
278      */
279     //-@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
280     private void processFiles(List<File> files) throws CheckstyleException {
281         for (final File file : files) {
282             String fileName = null;
283             try {
284                 fileName = file.getAbsolutePath();
285                 final long timestamp = file.lastModified();
286                 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
287                         || !acceptFileStarted(fileName)) {
288                     continue;
289                 }
290                 if (cacheFile != null) {
291                     cacheFile.put(fileName, timestamp);
292                 }
293                 fireFileStarted(fileName);
294                 final SortedSet<LocalizedMessage> fileMessages = processFile(file);
295                 fireErrors(fileName, fileMessages);
296                 fireFileFinished(fileName);
297             }
298             // -@cs[IllegalCatch] There is no other way to deliver filename that was under
299             // processing. See https://github.com/checkstyle/checkstyle/issues/2285
300             catch (Exception ex) {
301                 if (fileName != null && cacheFile != null) {
302                     cacheFile.remove(fileName);
303                 }
304 
305                 // We need to catch all exceptions to put a reason failure (file name) in exception
306                 throw new CheckstyleException("Exception was thrown while processing "
307                         + file.getPath(), ex);
308             }
309             catch (Error error) {
310                 if (fileName != null && cacheFile != null) {
311                     cacheFile.remove(fileName);
312                 }
313 
314                 // We need to catch all errors to put a reason failure (file name) in error
315                 throw new Error("Error was thrown while processing " + file.getPath(), error);
316             }
317         }
318     }
319 
320     /**
321      * Processes a file with all FileSetChecks.
322      * @param file a file to process.
323      * @return a sorted set of messages to be logged.
324      * @throws CheckstyleException if error condition within Checkstyle occurs.
325      * @noinspection ProhibitedExceptionThrown
326      */
327     private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException {
328         final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>();
329         try {
330             final FileText theText = new FileText(file.getAbsoluteFile(), charset);
331             for (final FileSetCheck fsc : fileSetChecks) {
332                 fileMessages.addAll(fsc.process(file, theText));
333             }
334         }
335         catch (final IOException ioe) {
336             log.debug("IOException occurred.", ioe);
337             fileMessages.add(new LocalizedMessage(1,
338                     Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
339                     new String[] {ioe.getMessage()}, null, getClass(), null));
340         }
341         // -@cs[IllegalCatch] There is no other way to obey haltOnException field
342         catch (Exception ex) {
343             if (haltOnException) {
344                 throw ex;
345             }
346 
347             log.debug("Exception occurred.", ex);
348 
349             final StringWriter sw = new StringWriter();
350             final PrintWriter pw = new PrintWriter(sw, true);
351 
352             ex.printStackTrace(pw);
353 
354             fileMessages.add(new LocalizedMessage(1,
355                     Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
356                     new String[] {sw.getBuffer().toString()},
357                     null, getClass(), null));
358         }
359         return fileMessages;
360     }
361 
362     /**
363      * Check if all before execution file filters accept starting the file.
364      *
365      * @param fileName
366      *            the file to be audited
367      * @return {@code true} if the file is accepted.
368      */
369     private boolean acceptFileStarted(String fileName) {
370         final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
371         return beforeExecutionFileFilters.accept(stripped);
372     }
373 
374     /**
375      * Notify all listeners about the beginning of a file audit.
376      *
377      * @param fileName
378      *            the file to be audited
379      */
380     @Override
381     public void fireFileStarted(String fileName) {
382         final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
383         final AuditEvent event = new AuditEvent(this, stripped);
384         for (final AuditListener listener : listeners) {
385             listener.fileStarted(event);
386         }
387     }
388 
389     /**
390      * Notify all listeners about the errors in a file.
391      *
392      * @param fileName the audited file
393      * @param errors the audit errors from the file
394      */
395     @Override
396     public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
397         final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
398         boolean hasNonFilteredViolations = false;
399         for (final LocalizedMessage element : errors) {
400             final AuditEvent event = new AuditEvent(this, stripped, element);
401             if (filters.accept(event)) {
402                 hasNonFilteredViolations = true;
403                 for (final AuditListener listener : listeners) {
404                     listener.addError(event);
405                 }
406             }
407         }
408         if (hasNonFilteredViolations && cacheFile != null) {
409             cacheFile.remove(fileName);
410         }
411     }
412 
413     /**
414      * Notify all listeners about the end of a file audit.
415      *
416      * @param fileName
417      *            the audited file
418      */
419     @Override
420     public void fireFileFinished(String fileName) {
421         final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
422         final AuditEvent event = new AuditEvent(this, stripped);
423         for (final AuditListener listener : listeners) {
424             listener.fileFinished(event);
425         }
426     }
427 
428     @Override
429     protected void finishLocalSetup() throws CheckstyleException {
430         final Locale locale = new Locale(localeLanguage, localeCountry);
431         LocalizedMessage.setLocale(locale);
432 
433         if (moduleFactory == null) {
434             if (moduleClassLoader == null) {
435                 throw new CheckstyleException(
436                         "if no custom moduleFactory is set, "
437                                 + "moduleClassLoader must be specified");
438             }
439 
440             final Set<String> packageNames = PackageNamesLoader
441                     .getPackageNames(moduleClassLoader);
442             moduleFactory = new PackageObjectFactory(packageNames,
443                     moduleClassLoader);
444         }
445 
446         final DefaultContext context = new DefaultContext();
447         context.add("charset", charset);
448         context.add("classLoader", classLoader);
449         context.add("moduleFactory", moduleFactory);
450         context.add("severity", severity.getName());
451         context.add("basedir", basedir);
452         context.add("tabWidth", String.valueOf(tabWidth));
453         childContext = context;
454     }
455 
456     /**
457      * {@inheritDoc} Creates child module.
458      * @noinspection ChainOfInstanceofChecks
459      */
460     @Override
461     protected void setupChild(Configuration childConf)
462             throws CheckstyleException {
463         final String name = childConf.getName();
464         final Object child;
465 
466         try {
467             child = moduleFactory.createModule(name);
468 
469             if (child instanceof AutomaticBean) {
470                 final AutomaticBean bean = (AutomaticBean) child;
471                 bean.contextualize(childContext);
472                 bean.configure(childConf);
473             }
474         }
475         catch (final CheckstyleException ex) {
476             throw new CheckstyleException("cannot initialize module " + name
477                     + " - " + ex.getMessage(), ex);
478         }
479         if (child instanceof FileSetCheck) {
480             final FileSetCheck fsc = (FileSetCheck) child;
481             fsc.init();
482             addFileSetCheck(fsc);
483         }
484         else if (child instanceof BeforeExecutionFileFilter) {
485             final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
486             addBeforeExecutionFileFilter(filter);
487         }
488         else if (child instanceof Filter) {
489             final Filter filter = (Filter) child;
490             addFilter(filter);
491         }
492         else if (child instanceof AuditListener) {
493             final AuditListener listener = (AuditListener) child;
494             addListener(listener);
495         }
496         else {
497             throw new CheckstyleException(name
498                     + " is not allowed as a child in Checker");
499         }
500     }
501 
502     /**
503      * Adds a FileSetCheck to the list of FileSetChecks
504      * that is executed in process().
505      * @param fileSetCheck the additional FileSetCheck
506      */
507     public void addFileSetCheck(FileSetCheck fileSetCheck) {
508         fileSetCheck.setMessageDispatcher(this);
509         fileSetChecks.add(fileSetCheck);
510     }
511 
512     /**
513      * Adds a before execution file filter to the end of the event chain.
514      * @param filter the additional filter
515      */
516     public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
517         beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
518     }
519 
520     /**
521      * Adds a filter to the end of the audit event filter chain.
522      * @param filter the additional filter
523      */
524     public void addFilter(Filter filter) {
525         filters.addFilter(filter);
526     }
527 
528     @Override
529     public final void addListener(AuditListener listener) {
530         listeners.add(listener);
531     }
532 
533     /**
534      * Sets the file extensions that identify the files that pass the
535      * filter of this FileSetCheck.
536      * @param extensions the set of file extensions. A missing
537      *     initial '.' character of an extension is automatically added.
538      */
539     public final void setFileExtensions(String... extensions) {
540         if (extensions == null) {
541             fileExtensions = null;
542         }
543         else {
544             fileExtensions = new String[extensions.length];
545             for (int i = 0; i < extensions.length; i++) {
546                 final String extension = extensions[i];
547                 if (CommonUtil.startsWithChar(extension, '.')) {
548                     fileExtensions[i] = extension;
549                 }
550                 else {
551                     fileExtensions[i] = "." + extension;
552                 }
553             }
554         }
555     }
556 
557     /**
558      * Sets the factory for creating submodules.
559      *
560      * @param moduleFactory the factory for creating FileSetChecks
561      */
562     public void setModuleFactory(ModuleFactory moduleFactory) {
563         this.moduleFactory = moduleFactory;
564     }
565 
566     /**
567      * Sets locale country.
568      * @param localeCountry the country to report messages
569      */
570     public void setLocaleCountry(String localeCountry) {
571         this.localeCountry = localeCountry;
572     }
573 
574     /**
575      * Sets locale language.
576      * @param localeLanguage the language to report messages
577      */
578     public void setLocaleLanguage(String localeLanguage) {
579         this.localeLanguage = localeLanguage;
580     }
581 
582     /**
583      * Sets the severity level.  The string should be one of the names
584      * defined in the {@code SeverityLevel} class.
585      *
586      * @param severity  The new severity level
587      * @see SeverityLevel
588      */
589     public final void setSeverity(String severity) {
590         this.severity = SeverityLevel.getInstance(severity);
591     }
592 
593     /**
594      * Sets the classloader that is used to contextualize fileset checks.
595      * Some Check implementations will use that classloader to improve the
596      * quality of their reports, e.g. to load a class and then analyze it via
597      * reflection.
598      * @param classLoader the new classloader
599      */
600     public final void setClassLoader(ClassLoader classLoader) {
601         this.classLoader = classLoader;
602     }
603 
604     @Override
605     public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
606         this.moduleClassLoader = moduleClassLoader;
607     }
608 
609     /**
610      * Sets a named charset.
611      * @param charset the name of a charset
612      * @throws UnsupportedEncodingException if charset is unsupported.
613      */
614     public void setCharset(String charset)
615             throws UnsupportedEncodingException {
616         if (!Charset.isSupported(charset)) {
617             final String message = "unsupported charset: '" + charset + "'";
618             throw new UnsupportedEncodingException(message);
619         }
620         this.charset = charset;
621     }
622 
623     /**
624      * Sets the field haltOnException.
625      * @param haltOnException the new value.
626      */
627     public void setHaltOnException(boolean haltOnException) {
628         this.haltOnException = haltOnException;
629     }
630 
631     /**
632      * Set the tab width to report errors with.
633      * @param tabWidth an {@code int} value
634      */
635     public final void setTabWidth(int tabWidth) {
636         this.tabWidth = tabWidth;
637     }
638 
639     /**
640      * Clears the cache.
641      */
642     public void clearCache() {
643         if (cacheFile != null) {
644             cacheFile.reset();
645         }
646     }
647 
648 }