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