001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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    /** Class loader to resolve classes with. **/
074    private ClassLoader classLoader = Thread.currentThread()
075            .getContextClassLoader();
076
077    /** The basedir to strip off in file names. */
078    private String basedir;
079
080    /** Locale country to report messages . **/
081    private String localeCountry = Locale.getDefault().getCountry();
082    /** Locale language to report messages . **/
083    private String localeLanguage = Locale.getDefault().getLanguage();
084
085    /** The factory for instantiating submodules. */
086    private ModuleFactory moduleFactory;
087
088    /** The classloader used for loading Checkstyle module classes. */
089    private ClassLoader moduleClassLoader;
090
091    /** The context of all child components. */
092    private Context childContext;
093
094    /** The audit event filters. */
095    private final FilterSet filters = new FilterSet();
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    /**
116     * Creates a new {@code Checker} instance.
117     * The instance needs to be contextualized and configured.
118     */
119    public Checker() {
120        addListener(counter);
121    }
122
123    @Override
124    public void finishLocalSetup() throws CheckstyleException {
125        final Locale locale = new Locale(localeLanguage, localeCountry);
126        LocalizedMessage.setLocale(locale);
127
128        if (moduleFactory == null) {
129
130            if (moduleClassLoader == null) {
131                throw new CheckstyleException(
132                        "if no custom moduleFactory is set, "
133                                + "moduleClassLoader must be specified");
134            }
135
136            final Set<String> packageNames = PackageNamesLoader
137                    .getPackageNames(moduleClassLoader);
138            moduleFactory = new PackageObjectFactory(packageNames,
139                    moduleClassLoader);
140        }
141
142        final DefaultContext context = new DefaultContext();
143        context.add("charset", charset);
144        context.add("classLoader", classLoader);
145        context.add("moduleFactory", moduleFactory);
146        context.add("severity", severityLevel.getName());
147        context.add("basedir", basedir);
148        childContext = context;
149    }
150
151    @Override
152    protected void setupChild(Configuration childConf)
153        throws CheckstyleException {
154        final String name = childConf.getName();
155        final Object child;
156
157        try {
158            child = moduleFactory.createModule(name);
159
160            if (child instanceof AutomaticBean) {
161                final AutomaticBean bean = (AutomaticBean) child;
162                bean.contextualize(childContext);
163                bean.configure(childConf);
164            }
165        }
166        catch (final CheckstyleException ex) {
167            throw new CheckstyleException("cannot initialize module " + name
168                    + " - " + ex.getMessage(), ex);
169        }
170        if (child instanceof FileSetCheck) {
171            final FileSetCheck fsc = (FileSetCheck) child;
172            fsc.init();
173            addFileSetCheck(fsc);
174        }
175        else if (child instanceof Filter) {
176            final Filter filter = (Filter) child;
177            addFilter(filter);
178        }
179        else if (child instanceof AuditListener) {
180            final AuditListener listener = (AuditListener) child;
181            addListener(listener);
182        }
183        else {
184            throw new CheckstyleException(name
185                    + " is not allowed as a child in Checker");
186        }
187    }
188
189    /**
190     * Adds a FileSetCheck to the list of FileSetChecks
191     * that is executed in process().
192     * @param fileSetCheck the additional FileSetCheck
193     */
194    public void addFileSetCheck(FileSetCheck fileSetCheck) {
195        fileSetCheck.setMessageDispatcher(this);
196        fileSetChecks.add(fileSetCheck);
197    }
198
199    /**
200     * Adds a filter to the end of the audit event filter chain.
201     * @param filter the additional filter
202     */
203    public void addFilter(Filter filter) {
204        filters.addFilter(filter);
205    }
206
207    /**
208     * Removes filter.
209     * @param filter filter to remove.
210     */
211    public void removeFilter(Filter filter) {
212        filters.removeFilter(filter);
213    }
214
215    /** Cleans up the object. **/
216    public void destroy() {
217        listeners.clear();
218        filters.clear();
219    }
220
221    /**
222     * Add the listener that will be used to receive events from the audit.
223     * @param listener the nosy thing
224     */
225    public final void addListener(AuditListener listener) {
226        listeners.add(listener);
227    }
228
229    /**
230     * Removes a given listener.
231     * @param listener a listener to remove
232     */
233    public void removeListener(AuditListener listener) {
234        listeners.remove(listener);
235    }
236
237    /**
238     * Processes a set of files with all FileSetChecks.
239     * Once this is done, it is highly recommended to call for
240     * the destroy method to close and remove the listeners.
241     * @param files the list of files to be audited.
242     * @return the total number of errors found
243     * @throws CheckstyleException if error condition within Checkstyle occurs
244     * @see #destroy()
245     */
246    public int process(List<File> files) throws CheckstyleException {
247        // Prepare to start
248        fireAuditStarted();
249        for (final FileSetCheck fsc : fileSetChecks) {
250            fsc.beginProcessing(charset);
251        }
252
253        // Process each file
254        for (final File file : files) {
255            if (!CommonUtils.matchesFileExtension(file, fileExtensions)) {
256                continue;
257            }
258            final String fileName = file.getAbsolutePath();
259            fireFileStarted(fileName);
260            final SortedSet<LocalizedMessage> fileMessages = Sets.newTreeSet();
261            try {
262                final FileText theText = new FileText(file.getAbsoluteFile(),
263                        charset);
264                for (final FileSetCheck fsc : fileSetChecks) {
265                    fileMessages.addAll(fsc.process(file, theText));
266                }
267            }
268            catch (final IOException ioe) {
269                LOG.debug("IOException occurred.", ioe);
270                fileMessages.add(new LocalizedMessage(0,
271                        Definitions.CHECKSTYLE_BUNDLE, "general.exception",
272                        new String[] {ioe.getMessage()}, null, getClass(),
273                        null));
274            }
275            fireErrors(fileName, fileMessages);
276            fireFileFinished(fileName);
277        }
278
279        // Finish up
280        for (final FileSetCheck fsc : fileSetChecks) {
281            // It may also log!!!
282            fsc.finishProcessing();
283        }
284
285        for (final FileSetCheck fsc : fileSetChecks) {
286            // It may also log!!!
287            fsc.destroy();
288        }
289
290        final int errorCount = counter.getCount();
291        fireAuditFinished();
292        return errorCount;
293    }
294
295    /**
296     * Sets base directory.
297     * @param basedir the base directory to strip off in file names
298     */
299    public void setBasedir(String basedir) {
300        this.basedir = basedir;
301    }
302
303    /** Notify all listeners about the audit start. */
304    void fireAuditStarted() {
305        final AuditEvent evt = new AuditEvent(this);
306        for (final AuditListener listener : listeners) {
307            listener.auditStarted(evt);
308        }
309    }
310
311    /** Notify all listeners about the audit end. */
312    void fireAuditFinished() {
313        final AuditEvent evt = new AuditEvent(this);
314        for (final AuditListener listener : listeners) {
315            listener.auditFinished(evt);
316        }
317    }
318
319    /**
320     * Notify all listeners about the beginning of a file audit.
321     *
322     * @param fileName
323     *            the file to be audited
324     */
325    @Override
326    public void fireFileStarted(String fileName) {
327        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
328        final AuditEvent evt = new AuditEvent(this, stripped);
329        for (final AuditListener listener : listeners) {
330            listener.fileStarted(evt);
331        }
332    }
333
334    /**
335     * Notify all listeners about the end of a file audit.
336     *
337     * @param fileName
338     *            the audited file
339     */
340    @Override
341    public void fireFileFinished(String fileName) {
342        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
343        final AuditEvent evt = new AuditEvent(this, stripped);
344        for (final AuditListener listener : listeners) {
345            listener.fileFinished(evt);
346        }
347    }
348
349    /**
350     * Notify all listeners about the errors in a file.
351     *
352     * @param fileName the audited file
353     * @param errors the audit errors from the file
354     */
355    @Override
356    public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
357        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
358        for (final LocalizedMessage element : errors) {
359            final AuditEvent evt = new AuditEvent(this, stripped, element);
360            if (filters.accept(evt)) {
361                for (final AuditListener listener : listeners) {
362                    listener.addError(evt);
363                }
364            }
365        }
366    }
367
368    /**
369     * Sets the file extensions that identify the files that pass the
370     * filter of this FileSetCheck.
371     * @param extensions the set of file extensions. A missing
372     *     initial '.' character of an extension is automatically added.
373     */
374    public final void setFileExtensions(String... extensions) {
375        if (extensions == null) {
376            fileExtensions = null;
377            return;
378        }
379
380        fileExtensions = new String[extensions.length];
381        for (int i = 0; i < extensions.length; i++) {
382            final String extension = extensions[i];
383            if (CommonUtils.startsWithChar(extension, '.')) {
384                fileExtensions[i] = extension;
385            }
386            else {
387                fileExtensions[i] = "." + extension;
388            }
389        }
390    }
391
392    /**
393     * Sets the factory for creating submodules.
394     *
395     * @param moduleFactory the factory for creating FileSetChecks
396     */
397    public void setModuleFactory(ModuleFactory moduleFactory) {
398        this.moduleFactory = moduleFactory;
399    }
400
401    /**
402     * Sets locale country.
403     * @param localeCountry the country to report messages
404     */
405    public void setLocaleCountry(String localeCountry) {
406        this.localeCountry = localeCountry;
407    }
408
409    /**
410     * Sets locale language.
411     * @param localeLanguage the language to report messages
412     */
413    public void setLocaleLanguage(String localeLanguage) {
414        this.localeLanguage = localeLanguage;
415    }
416
417    /**
418     * Sets the severity level.  The string should be one of the names
419     * defined in the {@code SeverityLevel} class.
420     *
421     * @param severity  The new severity level
422     * @see SeverityLevel
423     */
424    public final void setSeverity(String severity) {
425        severityLevel = SeverityLevel.getInstance(severity);
426    }
427
428    /**
429     * Sets the classloader that is used to contextualize fileset checks.
430     * Some Check implementations will use that classloader to improve the
431     * quality of their reports, e.g. to load a class and then analyze it via
432     * reflection.
433     * @param classLoader the new classloader
434     */
435    public final void setClassLoader(ClassLoader classLoader) {
436        this.classLoader = classLoader;
437    }
438
439    /**
440     * Sets the classloader that is used to contextualize fileset checks.
441     * Some Check implementations will use that classloader to improve the
442     * quality of their reports, e.g. to load a class and then analyze it via
443     * reflection.
444     * @param loader the new classloader
445     * @deprecated use {@link #setClassLoader(ClassLoader loader)} instead.
446     */
447    @Deprecated
448    public final void setClassloader(ClassLoader loader) {
449        classLoader = loader;
450    }
451
452    /**
453     * Sets the classloader used to load Checkstyle core and custom module
454     * classes when the module tree is being built up.
455     * If no custom ModuleFactory is being set for the Checker module then
456     * this module classloader must be specified.
457     * @param moduleClassLoader the classloader used to load module classes
458     */
459    public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
460        this.moduleClassLoader = moduleClassLoader;
461    }
462
463    /**
464     * Sets a named charset.
465     * @param charset the name of a charset
466     * @throws UnsupportedEncodingException if charset is unsupported.
467     */
468    public void setCharset(String charset)
469        throws UnsupportedEncodingException {
470        if (!Charset.isSupported(charset)) {
471            final String message = "unsupported charset: '" + charset + "'";
472            throw new UnsupportedEncodingException(message);
473        }
474        this.charset = charset;
475    }
476}