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