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