001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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    /** Class loader to resolve classes with. **/
090    private ClassLoader classLoader = Thread.currentThread()
091            .getContextClassLoader();
092
093    /** The basedir to strip off in file names. */
094    private String basedir;
095
096    /** Locale country to report messages . **/
097    private String localeCountry = Locale.getDefault().getCountry();
098    /** Locale language to report messages . **/
099    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}