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