001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.PrintWriter;
025import java.io.StringWriter;
026import java.io.UnsupportedEncodingException;
027import java.nio.charset.Charset;
028import java.nio.charset.StandardCharsets;
029import java.util.ArrayList;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Locale;
033import java.util.Set;
034import java.util.SortedSet;
035import java.util.TreeSet;
036import java.util.stream.Collectors;
037
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040
041import com.puppycrawl.tools.checkstyle.api.AuditEvent;
042import com.puppycrawl.tools.checkstyle.api.AuditListener;
043import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
044import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
045import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
046import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
047import com.puppycrawl.tools.checkstyle.api.Configuration;
048import com.puppycrawl.tools.checkstyle.api.Context;
049import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
050import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
051import com.puppycrawl.tools.checkstyle.api.FileText;
052import com.puppycrawl.tools.checkstyle.api.Filter;
053import com.puppycrawl.tools.checkstyle.api.FilterSet;
054import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
055import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
056import com.puppycrawl.tools.checkstyle.api.RootModule;
057import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
058import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
059import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
060
061/**
062 * This class provides the functionality to check a set of files.
063 */
064public class Checker extends AutomaticBean implements MessageDispatcher, RootModule {
065
066    /** Message to use when an exception occurs and should be printed as a violation. */
067    public static final String EXCEPTION_MSG = "general.exception";
068
069    /** Logger for Checker. */
070    private final Log log;
071
072    /** Maintains error count. */
073    private final SeverityLevelCounter counter = new SeverityLevelCounter(
074            SeverityLevel.ERROR);
075
076    /** Vector of listeners. */
077    private final List<AuditListener> listeners = new ArrayList<>();
078
079    /** Vector of fileset checks. */
080    private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
081
082    /** The audit event before execution file filters. */
083    private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
084            new BeforeExecutionFileFilterSet();
085
086    /** The audit event filters. */
087    private final FilterSet filters = new FilterSet();
088
089    /** The basedir to strip off in file names. */
090    private String basedir;
091
092    /** Locale country to report messages . **/
093    private String localeCountry = Locale.getDefault().getCountry();
094    /** Locale language to report messages . **/
095    private String localeLanguage = Locale.getDefault().getLanguage();
096
097    /** The factory for instantiating submodules. */
098    private ModuleFactory moduleFactory;
099
100    /** The classloader used for loading Checkstyle module classes. */
101    private ClassLoader moduleClassLoader;
102
103    /** The context of all child components. */
104    private Context childContext;
105
106    /** The file extensions that are accepted. */
107    private String[] fileExtensions = CommonUtil.EMPTY_STRING_ARRAY;
108
109    /**
110     * The severity level of any violations found by submodules.
111     * The value of this property is passed to submodules via
112     * contextualize().
113     *
114     * <p>Note: Since the Checker is merely a container for modules
115     * it does not make sense to implement logging functionality
116     * here. Consequently Checker does not extend AbstractViolationReporter,
117     * leading to a bit of duplicated code for severity level setting.
118     */
119    private SeverityLevel severity = SeverityLevel.ERROR;
120
121    /** Name of a charset. */
122    private String charset = StandardCharsets.UTF_8.name();
123
124    /** Cache file. **/
125    private PropertyCacheFile cacheFile;
126
127    /** Controls whether exceptions should halt execution or not. */
128    private boolean haltOnException = true;
129
130    /** The tab width for column reporting. */
131    private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
132
133    /**
134     * Creates a new {@code Checker} instance.
135     * The instance needs to be contextualized and configured.
136     */
137    public Checker() {
138        addListener(counter);
139        log = LogFactory.getLog(Checker.class);
140    }
141
142    /**
143     * Sets cache file.
144     *
145     * @param fileName the cache file.
146     * @throws IOException if there are some problems with file loading.
147     */
148    public void setCacheFile(String fileName) throws IOException {
149        final Configuration configuration = getConfiguration();
150        cacheFile = new PropertyCacheFile(configuration, fileName);
151        cacheFile.load();
152    }
153
154    /**
155     * Removes before execution file filter.
156     *
157     * @param filter before execution file filter to remove.
158     */
159    public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
160        beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
161    }
162
163    /**
164     * Removes filter.
165     *
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 (cacheFile != null) {
179            try {
180                cacheFile.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     *
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     *
200     * @param basedir the base directory to strip off in file names
201     */
202    public void setBasedir(String basedir) {
203        this.basedir = basedir;
204    }
205
206    @Override
207    public int process(List<File> files) throws CheckstyleException {
208        if (cacheFile != null) {
209            cacheFile.putExternalResources(getExternalResourceLocations());
210        }
211
212        // Prepare to start
213        fireAuditStarted();
214        for (final FileSetCheck fsc : fileSetChecks) {
215            fsc.beginProcessing(charset);
216        }
217
218        final List<File> targetFiles = files.stream()
219                .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
220                .collect(Collectors.toList());
221        processFiles(targetFiles);
222
223        // Finish up
224        // It may also log!!!
225        fileSetChecks.forEach(FileSetCheck::finishProcessing);
226
227        // It may also log!!!
228        fileSetChecks.forEach(FileSetCheck::destroy);
229
230        final int errorCount = counter.getCount();
231        fireAuditFinished();
232        return errorCount;
233    }
234
235    /**
236     * Returns a set of external configuration resource locations which are used by all file set
237     * checks and filters.
238     *
239     * @return a set of external configuration resource locations which are used by all file set
240     *         checks and filters.
241     */
242    private Set<String> getExternalResourceLocations() {
243        final Set<String> externalResources = new HashSet<>();
244        fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder)
245            .forEach(check -> {
246                final Set<String> locations =
247                    ((ExternalResourceHolder) check).getExternalResourceLocations();
248                externalResources.addAll(locations);
249            });
250        filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder)
251            .forEach(filter -> {
252                final Set<String> locations =
253                    ((ExternalResourceHolder) filter).getExternalResourceLocations();
254                externalResources.addAll(locations);
255            });
256        return externalResources;
257    }
258
259    /** Notify all listeners about the audit start. */
260    private void fireAuditStarted() {
261        final AuditEvent event = new AuditEvent(this);
262        for (final AuditListener listener : listeners) {
263            listener.auditStarted(event);
264        }
265    }
266
267    /** Notify all listeners about the audit end. */
268    private void fireAuditFinished() {
269        final AuditEvent event = new AuditEvent(this);
270        for (final AuditListener listener : listeners) {
271            listener.auditFinished(event);
272        }
273    }
274
275    /**
276     * Processes a list of files with all FileSetChecks.
277     *
278     * @param files a list of files to process.
279     * @throws CheckstyleException if error condition within Checkstyle occurs.
280     * @throws Error wraps any java.lang.Error happened during execution
281     * @noinspection ProhibitedExceptionThrown
282     */
283    // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
284    private void processFiles(List<File> files) throws CheckstyleException {
285        for (final File file : files) {
286            String fileName = null;
287            try {
288                fileName = file.getAbsolutePath();
289                final long timestamp = file.lastModified();
290                if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
291                        || !acceptFileStarted(fileName)) {
292                    continue;
293                }
294                if (cacheFile != null) {
295                    cacheFile.put(fileName, timestamp);
296                }
297                fireFileStarted(fileName);
298                final SortedSet<LocalizedMessage> fileMessages = processFile(file);
299                fireErrors(fileName, fileMessages);
300                fireFileFinished(fileName);
301            }
302            // -@cs[IllegalCatch] There is no other way to deliver filename that was under
303            // processing. See https://github.com/checkstyle/checkstyle/issues/2285
304            catch (Exception ex) {
305                if (fileName != null && cacheFile != null) {
306                    cacheFile.remove(fileName);
307                }
308
309                // We need to catch all exceptions to put a reason failure (file name) in exception
310                throw new CheckstyleException("Exception was thrown while processing "
311                        + file.getPath(), ex);
312            }
313            catch (Error error) {
314                if (fileName != null && cacheFile != null) {
315                    cacheFile.remove(fileName);
316                }
317
318                // We need to catch all errors to put a reason failure (file name) in error
319                throw new Error("Error was thrown while processing " + file.getPath(), error);
320            }
321        }
322    }
323
324    /**
325     * Processes a file with all FileSetChecks.
326     *
327     * @param file a file to process.
328     * @return a sorted set of messages to be logged.
329     * @throws CheckstyleException if error condition within Checkstyle occurs.
330     * @noinspection ProhibitedExceptionThrown
331     */
332    private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException {
333        final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>();
334        try {
335            final FileText theText = new FileText(file.getAbsoluteFile(), charset);
336            for (final FileSetCheck fsc : fileSetChecks) {
337                fileMessages.addAll(fsc.process(file, theText));
338            }
339        }
340        catch (final IOException ioe) {
341            log.debug("IOException occurred.", ioe);
342            fileMessages.add(new LocalizedMessage(1,
343                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
344                    new String[] {ioe.getMessage()}, null, getClass(), null));
345        }
346        // -@cs[IllegalCatch] There is no other way to obey haltOnException field
347        catch (Exception ex) {
348            if (haltOnException) {
349                throw ex;
350            }
351
352            log.debug("Exception occurred.", ex);
353
354            final StringWriter sw = new StringWriter();
355            final PrintWriter pw = new PrintWriter(sw, true);
356
357            ex.printStackTrace(pw);
358
359            fileMessages.add(new LocalizedMessage(1,
360                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
361                    new String[] {sw.getBuffer().toString()},
362                    null, getClass(), null));
363        }
364        return fileMessages;
365    }
366
367    /**
368     * Check if all before execution file filters accept starting the file.
369     *
370     * @param fileName
371     *            the file to be audited
372     * @return {@code true} if the file is accepted.
373     */
374    private boolean acceptFileStarted(String fileName) {
375        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
376        return beforeExecutionFileFilters.accept(stripped);
377    }
378
379    /**
380     * Notify all listeners about the beginning of a file audit.
381     *
382     * @param fileName
383     *            the file to be audited
384     */
385    @Override
386    public void fireFileStarted(String fileName) {
387        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
388        final AuditEvent event = new AuditEvent(this, stripped);
389        for (final AuditListener listener : listeners) {
390            listener.fileStarted(event);
391        }
392    }
393
394    /**
395     * Notify all listeners about the errors in a file.
396     *
397     * @param fileName the audited file
398     * @param errors the audit errors from the file
399     */
400    @Override
401    public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
402        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
403        boolean hasNonFilteredViolations = false;
404        for (final LocalizedMessage element : errors) {
405            final AuditEvent event = new AuditEvent(this, stripped, element);
406            if (filters.accept(event)) {
407                hasNonFilteredViolations = true;
408                for (final AuditListener listener : listeners) {
409                    listener.addError(event);
410                }
411            }
412        }
413        if (hasNonFilteredViolations && cacheFile != null) {
414            cacheFile.remove(fileName);
415        }
416    }
417
418    /**
419     * Notify all listeners about the end of a file audit.
420     *
421     * @param fileName
422     *            the audited file
423     */
424    @Override
425    public void fireFileFinished(String fileName) {
426        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
427        final AuditEvent event = new AuditEvent(this, stripped);
428        for (final AuditListener listener : listeners) {
429            listener.fileFinished(event);
430        }
431    }
432
433    @Override
434    protected void finishLocalSetup() throws CheckstyleException {
435        final Locale locale = new Locale(localeLanguage, localeCountry);
436        LocalizedMessage.setLocale(locale);
437
438        if (moduleFactory == null) {
439            if (moduleClassLoader == null) {
440                throw new CheckstyleException(
441                        "if no custom moduleFactory is set, "
442                                + "moduleClassLoader must be specified");
443            }
444
445            final Set<String> packageNames = PackageNamesLoader
446                    .getPackageNames(moduleClassLoader);
447            moduleFactory = new PackageObjectFactory(packageNames,
448                    moduleClassLoader);
449        }
450
451        final DefaultContext context = new DefaultContext();
452        context.add("charset", charset);
453        context.add("moduleFactory", moduleFactory);
454        context.add("severity", severity.getName());
455        context.add("basedir", basedir);
456        context.add("tabWidth", String.valueOf(tabWidth));
457        childContext = context;
458    }
459
460    /**
461     * {@inheritDoc} Creates child module.
462     *
463     * @noinspection ChainOfInstanceofChecks
464     */
465    @Override
466    protected void setupChild(Configuration childConf)
467            throws CheckstyleException {
468        final String name = childConf.getName();
469        final Object child;
470
471        try {
472            child = moduleFactory.createModule(name);
473
474            if (child instanceof AutomaticBean) {
475                final AutomaticBean bean = (AutomaticBean) child;
476                bean.contextualize(childContext);
477                bean.configure(childConf);
478            }
479        }
480        catch (final CheckstyleException ex) {
481            throw new CheckstyleException("cannot initialize module " + name
482                    + " - " + ex.getMessage(), ex);
483        }
484        if (child instanceof FileSetCheck) {
485            final FileSetCheck fsc = (FileSetCheck) child;
486            fsc.init();
487            addFileSetCheck(fsc);
488        }
489        else if (child instanceof BeforeExecutionFileFilter) {
490            final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
491            addBeforeExecutionFileFilter(filter);
492        }
493        else if (child instanceof Filter) {
494            final Filter filter = (Filter) child;
495            addFilter(filter);
496        }
497        else if (child instanceof AuditListener) {
498            final AuditListener listener = (AuditListener) child;
499            addListener(listener);
500        }
501        else {
502            throw new CheckstyleException(name
503                    + " is not allowed as a child in Checker");
504        }
505    }
506
507    /**
508     * Adds a FileSetCheck to the list of FileSetChecks
509     * that is executed in process().
510     *
511     * @param fileSetCheck the additional FileSetCheck
512     */
513    public void addFileSetCheck(FileSetCheck fileSetCheck) {
514        fileSetCheck.setMessageDispatcher(this);
515        fileSetChecks.add(fileSetCheck);
516    }
517
518    /**
519     * Adds a before execution file filter to the end of the event chain.
520     *
521     * @param filter the additional filter
522     */
523    public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
524        beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
525    }
526
527    /**
528     * Adds a filter to the end of the audit event filter chain.
529     *
530     * @param filter the additional filter
531     */
532    public void addFilter(Filter filter) {
533        filters.addFilter(filter);
534    }
535
536    @Override
537    public final void addListener(AuditListener listener) {
538        listeners.add(listener);
539    }
540
541    /**
542     * Sets the file extensions that identify the files that pass the
543     * filter of this FileSetCheck.
544     *
545     * @param extensions the set of file extensions. A missing
546     *     initial '.' character of an extension is automatically added.
547     */
548    public final void setFileExtensions(String... extensions) {
549        if (extensions == null) {
550            fileExtensions = null;
551        }
552        else {
553            fileExtensions = new String[extensions.length];
554            for (int i = 0; i < extensions.length; i++) {
555                final String extension = extensions[i];
556                if (CommonUtil.startsWithChar(extension, '.')) {
557                    fileExtensions[i] = extension;
558                }
559                else {
560                    fileExtensions[i] = "." + extension;
561                }
562            }
563        }
564    }
565
566    /**
567     * Sets the factory for creating submodules.
568     *
569     * @param moduleFactory the factory for creating FileSetChecks
570     */
571    public void setModuleFactory(ModuleFactory moduleFactory) {
572        this.moduleFactory = moduleFactory;
573    }
574
575    /**
576     * Sets locale country.
577     *
578     * @param localeCountry the country to report messages
579     */
580    public void setLocaleCountry(String localeCountry) {
581        this.localeCountry = localeCountry;
582    }
583
584    /**
585     * Sets locale language.
586     *
587     * @param localeLanguage the language to report messages
588     */
589    public void setLocaleLanguage(String localeLanguage) {
590        this.localeLanguage = localeLanguage;
591    }
592
593    /**
594     * Sets the severity level.  The string should be one of the names
595     * defined in the {@code SeverityLevel} class.
596     *
597     * @param severity  The new severity level
598     * @see SeverityLevel
599     */
600    public final void setSeverity(String severity) {
601        this.severity = SeverityLevel.getInstance(severity);
602    }
603
604    @Override
605    public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
606        this.moduleClassLoader = moduleClassLoader;
607    }
608
609    /**
610     * Sets a named charset.
611     *
612     * @param charset the name of a charset
613     * @throws UnsupportedEncodingException if charset is unsupported.
614     */
615    public void setCharset(String charset)
616            throws UnsupportedEncodingException {
617        if (!Charset.isSupported(charset)) {
618            final String message = "unsupported charset: '" + charset + "'";
619            throw new UnsupportedEncodingException(message);
620        }
621        this.charset = charset;
622    }
623
624    /**
625     * Sets the field haltOnException.
626     *
627     * @param haltOnException the new value.
628     */
629    public void setHaltOnException(boolean haltOnException) {
630        this.haltOnException = haltOnException;
631    }
632
633    /**
634     * Set the tab width to report audit events with.
635     *
636     * @param tabWidth an {@code int} value
637     */
638    public final void setTabWidth(int tabWidth) {
639        this.tabWidth = tabWidth;
640    }
641
642    /**
643     * Clears the cache.
644     */
645    public void clearCache() {
646        if (cacheFile != null) {
647            cacheFile.reset();
648        }
649    }
650
651}