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