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            try {
256                if (!CommonUtils.matchesFileExtension(file, fileExtensions)) {
257                    continue;
258                }
259                final String fileName = file.getAbsolutePath();
260                fireFileStarted(fileName);
261                final SortedSet<LocalizedMessage> fileMessages = Sets.newTreeSet();
262                try {
263                    final FileText theText = new FileText(file.getAbsoluteFile(),
264                            charset);
265                    for (final FileSetCheck fsc : fileSetChecks) {
266                        fileMessages.addAll(fsc.process(file, theText));
267                    }
268                }
269                catch (final IOException ioe) {
270                    LOG.debug("IOException occurred.", ioe);
271                    fileMessages.add(new LocalizedMessage(0,
272                            Definitions.CHECKSTYLE_BUNDLE, "general.exception",
273                            new String[] {ioe.getMessage()}, null, getClass(),
274                            null));
275                }
276                fireErrors(fileName, fileMessages);
277                fireFileFinished(fileName);
278            }
279            catch (Exception ex) {
280                // We need to catch all exception to put a reason failure(file name) in exception
281                throw new CheckstyleException("Exception was thrown while processing "
282                        + file.getPath(), ex);
283            }
284        }
285
286        // Finish up
287        for (final FileSetCheck fsc : fileSetChecks) {
288            // It may also log!!!
289            fsc.finishProcessing();
290        }
291
292        for (final FileSetCheck fsc : fileSetChecks) {
293            // It may also log!!!
294            fsc.destroy();
295        }
296
297        final int errorCount = counter.getCount();
298        fireAuditFinished();
299        return errorCount;
300    }
301
302    /**
303     * Sets base directory.
304     * @param basedir the base directory to strip off in file names
305     */
306    public void setBasedir(String basedir) {
307        this.basedir = basedir;
308    }
309
310    /** Notify all listeners about the audit start. */
311    void fireAuditStarted() {
312        final AuditEvent event = new AuditEvent(this);
313        for (final AuditListener listener : listeners) {
314            listener.auditStarted(event);
315        }
316    }
317
318    /** Notify all listeners about the audit end. */
319    void fireAuditFinished() {
320        final AuditEvent event = new AuditEvent(this);
321        for (final AuditListener listener : listeners) {
322            listener.auditFinished(event);
323        }
324    }
325
326    /**
327     * Notify all listeners about the beginning of a file audit.
328     *
329     * @param fileName
330     *            the file to be audited
331     */
332    @Override
333    public void fireFileStarted(String fileName) {
334        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
335        final AuditEvent event = new AuditEvent(this, stripped);
336        for (final AuditListener listener : listeners) {
337            listener.fileStarted(event);
338        }
339    }
340
341    /**
342     * Notify all listeners about the end of a file audit.
343     *
344     * @param fileName
345     *            the audited file
346     */
347    @Override
348    public void fireFileFinished(String fileName) {
349        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
350        final AuditEvent event = new AuditEvent(this, stripped);
351        for (final AuditListener listener : listeners) {
352            listener.fileFinished(event);
353        }
354    }
355
356    /**
357     * Notify all listeners about the errors in a file.
358     *
359     * @param fileName the audited file
360     * @param errors the audit errors from the file
361     */
362    @Override
363    public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
364        final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
365        for (final LocalizedMessage element : errors) {
366            final AuditEvent event = new AuditEvent(this, stripped, element);
367            if (filters.accept(event)) {
368                for (final AuditListener listener : listeners) {
369                    listener.addError(event);
370                }
371            }
372        }
373    }
374
375    /**
376     * Sets the file extensions that identify the files that pass the
377     * filter of this FileSetCheck.
378     * @param extensions the set of file extensions. A missing
379     *     initial '.' character of an extension is automatically added.
380     */
381    public final void setFileExtensions(String... extensions) {
382        if (extensions == null) {
383            fileExtensions = null;
384            return;
385        }
386
387        fileExtensions = new String[extensions.length];
388        for (int i = 0; i < extensions.length; i++) {
389            final String extension = extensions[i];
390            if (CommonUtils.startsWithChar(extension, '.')) {
391                fileExtensions[i] = extension;
392            }
393            else {
394                fileExtensions[i] = "." + extension;
395            }
396        }
397    }
398
399    /**
400     * Sets the factory for creating submodules.
401     *
402     * @param moduleFactory the factory for creating FileSetChecks
403     */
404    public void setModuleFactory(ModuleFactory moduleFactory) {
405        this.moduleFactory = moduleFactory;
406    }
407
408    /**
409     * Sets locale country.
410     * @param localeCountry the country to report messages
411     */
412    public void setLocaleCountry(String localeCountry) {
413        this.localeCountry = localeCountry;
414    }
415
416    /**
417     * Sets locale language.
418     * @param localeLanguage the language to report messages
419     */
420    public void setLocaleLanguage(String localeLanguage) {
421        this.localeLanguage = localeLanguage;
422    }
423
424    /**
425     * Sets the severity level.  The string should be one of the names
426     * defined in the {@code SeverityLevel} class.
427     *
428     * @param severity  The new severity level
429     * @see SeverityLevel
430     */
431    public final void setSeverity(String severity) {
432        severityLevel = SeverityLevel.getInstance(severity);
433    }
434
435    /**
436     * Sets the classloader that is used to contextualize fileset checks.
437     * Some Check implementations will use that classloader to improve the
438     * quality of their reports, e.g. to load a class and then analyze it via
439     * reflection.
440     * @param classLoader the new classloader
441     */
442    public final void setClassLoader(ClassLoader classLoader) {
443        this.classLoader = classLoader;
444    }
445
446    /**
447     * Sets the classloader that is used to contextualize fileset checks.
448     * Some Check implementations will use that classloader to improve the
449     * quality of their reports, e.g. to load a class and then analyze it via
450     * reflection.
451     * @param loader the new classloader
452     * @deprecated use {@link #setClassLoader(ClassLoader loader)} instead.
453     */
454    @Deprecated
455    public final void setClassloader(ClassLoader loader) {
456        classLoader = loader;
457    }
458
459    /**
460     * Sets the classloader used to load Checkstyle core and custom module
461     * classes when the module tree is being built up.
462     * If no custom ModuleFactory is being set for the Checker module then
463     * this module classloader must be specified.
464     * @param moduleClassLoader the classloader used to load module classes
465     */
466    public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
467        this.moduleClassLoader = moduleClassLoader;
468    }
469
470    /**
471     * Sets a named charset.
472     * @param charset the name of a charset
473     * @throws UnsupportedEncodingException if charset is unsupported.
474     */
475    public void setCharset(String charset)
476        throws UnsupportedEncodingException {
477        if (!Charset.isSupported(charset)) {
478            final String message = "unsupported charset: '" + charset + "'";
479            throw new UnsupportedEncodingException(message);
480        }
481        this.charset = charset;
482    }
483}