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.CommonUtils;
059
060/**
061 * This class provides the functionality to check a set of files.
062 * @author Oliver Burn
063 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
064 * @author lkuehne
065 * @author Andrei Selkin
066 */
067public class Checker extends AutomaticBean implements MessageDispatcher, RootModule {
068
069    /** Message to use when an exception occurs and should be printed as a violation. */
070    public static final String EXCEPTION_MSG = "general.exception";
071
072    /** Logger for Checker. */
073    private final Log log;
074
075    /** Maintains error count. */
076    private final SeverityLevelCounter counter = new SeverityLevelCounter(
077            SeverityLevel.ERROR);
078
079    /** Vector of listeners. */
080    private final List<AuditListener> listeners = new ArrayList<>();
081
082    /** Vector of fileset checks. */
083    private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
084
085    /** The audit event before execution file filters. */
086    private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
087            new BeforeExecutionFileFilterSet();
088
089    /** The audit event filters. */
090    private final FilterSet filters = new FilterSet();
091
092    /** Class loader to resolve classes with. **/
093    private ClassLoader classLoader = Thread.currentThread()
094            .getContextClassLoader();
095
096    /** The basedir to strip off in file names. */
097    private String basedir;
098
099    /** Locale country to report messages . **/
100    private String localeCountry = Locale.getDefault().getCountry();
101    /** Locale language to report messages . **/
102    private String localeLanguage = Locale.getDefault().getLanguage();
103
104    /** The factory for instantiating submodules. */
105    private ModuleFactory moduleFactory;
106
107    /** The classloader used for loading Checkstyle module classes. */
108    private ClassLoader moduleClassLoader;
109
110    /** The context of all child components. */
111    private Context childContext;
112
113    /** The file extensions that are accepted. */
114    private String[] fileExtensions = CommonUtils.EMPTY_STRING_ARRAY;
115
116    /**
117     * The severity level of any violations found by submodules.
118     * The value of this property is passed to submodules via
119     * contextualize().
120     *
121     * <p>Note: Since the Checker is merely a container for modules
122     * it does not make sense to implement logging functionality
123     * here. Consequently Checker does not extend AbstractViolationReporter,
124     * leading to a bit of duplicated code for severity level setting.
125     */
126    private SeverityLevel severity = SeverityLevel.ERROR;
127
128    /** Name of a charset. */
129    private String charset = System.getProperty("file.encoding", StandardCharsets.UTF_8.name());
130
131    /** Cache file. **/
132    private PropertyCacheFile cacheFile;
133
134    /** Controls whether exceptions should halt execution or not. */
135    private boolean haltOnException = true;
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        processFiles(files);
218
219        // Finish up
220        // It may also log!!!
221        fileSetChecks.forEach(FileSetCheck::finishProcessing);
222
223        // It may also log!!!
224        fileSetChecks.forEach(FileSetCheck::destroy);
225
226        final int errorCount = counter.getCount();
227        fireAuditFinished();
228        return errorCount;
229    }
230
231    /**
232     * Returns a set of external configuration resource locations which are used by all file set
233     * checks and filters.
234     * @return a set of external configuration resource locations which are used by all file set
235     *         checks and filters.
236     */
237    private Set<String> getExternalResourceLocations() {
238        final Set<String> externalResources = new HashSet<>();
239        fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder)
240            .forEach(check -> {
241                final Set<String> locations =
242                    ((ExternalResourceHolder) check).getExternalResourceLocations();
243                externalResources.addAll(locations);
244            });
245        filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder)
246            .forEach(filter -> {
247                final Set<String> locations =
248                    ((ExternalResourceHolder) filter).getExternalResourceLocations();
249                externalResources.addAll(locations);
250            });
251        return externalResources;
252    }
253
254    /** Notify all listeners about the audit start. */
255    private void fireAuditStarted() {
256        final AuditEvent event = new AuditEvent(this);
257        for (final AuditListener listener : listeners) {
258            listener.auditStarted(event);
259        }
260    }
261
262    /** Notify all listeners about the audit end. */
263    private void fireAuditFinished() {
264        final AuditEvent event = new AuditEvent(this);
265        for (final AuditListener listener : listeners) {
266            listener.auditFinished(event);
267        }
268    }
269
270    /**
271     * Processes a list of files with all FileSetChecks.
272     * @param files a list of files to process.
273     * @throws CheckstyleException if error condition within Checkstyle occurs.
274     * @noinspection ProhibitedExceptionThrown
275     */
276    private void processFiles(List<File> files) throws CheckstyleException {
277        for (final File file : files) {
278            try {
279                final String fileName = file.getAbsolutePath();
280                final long timestamp = file.lastModified();
281                if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
282                        || !CommonUtils.matchesFileExtension(file, fileExtensions)
283                        || !acceptFileStarted(fileName)) {
284                    continue;
285                }
286                if (cacheFile != null) {
287                    cacheFile.put(fileName, timestamp);
288                }
289                fireFileStarted(fileName);
290                final SortedSet<LocalizedMessage> fileMessages = processFile(file);
291                fireErrors(fileName, fileMessages);
292                fireFileFinished(fileName);
293            }
294            // -@cs[IllegalCatch] There is no other way to deliver filename that was under
295            // processing. See https://github.com/checkstyle/checkstyle/issues/2285
296            catch (Exception ex) {
297                // We need to catch all exceptions to put a reason failure (file name) in exception
298                throw new CheckstyleException("Exception was thrown while processing "
299                        + file.getPath(), ex);
300            }
301            catch (Error error) {
302                // We need to catch all errors to put a reason failure (file name) in error
303                throw new Error("Error was thrown while processing " + file.getPath(), error);
304            }
305        }
306    }
307
308    /**
309     * Processes a file with all FileSetChecks.
310     * @param file a file to process.
311     * @return a sorted set of messages to be logged.
312     * @throws CheckstyleException if error condition within Checkstyle occurs.
313     * @noinspection ProhibitedExceptionThrown
314     */
315    private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException {
316        final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>();
317        try {
318            final FileText theText = new FileText(file.getAbsoluteFile(), charset);
319            for (final FileSetCheck fsc : fileSetChecks) {
320                fileMessages.addAll(fsc.process(file, theText));
321            }
322        }
323        catch (final IOException ioe) {
324            log.debug("IOException occurred.", ioe);
325            fileMessages.add(new LocalizedMessage(0,
326                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
327                    new String[] {ioe.getMessage()}, null, getClass(), null));
328        }
329        // -@cs[IllegalCatch] There is no other way to obey haltOnException field
330        catch (Exception ex) {
331            if (haltOnException) {
332                throw ex;
333            }
334
335            log.debug("Exception occurred.", ex);
336
337            final StringWriter sw = new StringWriter();
338            final PrintWriter pw = new PrintWriter(sw, true);
339
340            ex.printStackTrace(pw);
341
342            fileMessages.add(new LocalizedMessage(0,
343                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
344                    new String[] {sw.getBuffer().toString()},
345                    null, getClass(), null));
346        }
347        return fileMessages;
348    }
349
350    /**
351     * Check if all before execution file filters accept starting the file.
352     *
353     * @param fileName
354     *            the file to be audited
355     * @return {@code true} if the file is accepted.
356     */
357    private boolean acceptFileStarted(String fileName) {
358        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
359        return beforeExecutionFileFilters.accept(stripped);
360    }
361
362    /**
363     * Notify all listeners about the beginning of a file audit.
364     *
365     * @param fileName
366     *            the file to be audited
367     */
368    @Override
369    public void fireFileStarted(String fileName) {
370        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
371        final AuditEvent event = new AuditEvent(this, stripped);
372        for (final AuditListener listener : listeners) {
373            listener.fileStarted(event);
374        }
375    }
376
377    /**
378     * Notify all listeners about the errors in a file.
379     *
380     * @param fileName the audited file
381     * @param errors the audit errors from the file
382     */
383    @Override
384    public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
385        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
386        boolean hasNonFilteredViolations = false;
387        for (final LocalizedMessage element : errors) {
388            final AuditEvent event = new AuditEvent(this, stripped, element);
389            if (filters.accept(event)) {
390                hasNonFilteredViolations = true;
391                for (final AuditListener listener : listeners) {
392                    listener.addError(event);
393                }
394            }
395        }
396        if (hasNonFilteredViolations && cacheFile != null) {
397            cacheFile.remove(fileName);
398        }
399    }
400
401    /**
402     * Notify all listeners about the end of a file audit.
403     *
404     * @param fileName
405     *            the audited file
406     */
407    @Override
408    public void fireFileFinished(String fileName) {
409        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
410        final AuditEvent event = new AuditEvent(this, stripped);
411        for (final AuditListener listener : listeners) {
412            listener.fileFinished(event);
413        }
414    }
415
416    @Override
417    protected void finishLocalSetup() throws CheckstyleException {
418        final Locale locale = new Locale(localeLanguage, localeCountry);
419        LocalizedMessage.setLocale(locale);
420
421        if (moduleFactory == null) {
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", severity.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        this.severity = 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 (cacheFile != null) {
623            cacheFile.reset();
624        }
625    }
626
627}