001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle;
020
021import com.google.common.collect.Lists;
022import com.google.common.collect.Sets;
023import com.puppycrawl.tools.checkstyle.api.AuditEvent;
024import com.puppycrawl.tools.checkstyle.api.AuditListener;
025import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
026import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
027import com.puppycrawl.tools.checkstyle.api.Configuration;
028import com.puppycrawl.tools.checkstyle.api.Context;
029import com.puppycrawl.tools.checkstyle.api.FastStack;
030import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
031import com.puppycrawl.tools.checkstyle.api.FileText;
032import com.puppycrawl.tools.checkstyle.api.Filter;
033import com.puppycrawl.tools.checkstyle.api.FilterSet;
034import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
035import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
036import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
037import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
038import com.puppycrawl.tools.checkstyle.api.Utils;
039
040import java.io.File;
041import java.io.FileNotFoundException;
042import java.io.IOException;
043import java.io.UnsupportedEncodingException;
044import java.nio.charset.Charset;
045import java.util.List;
046import java.util.Locale;
047import java.util.Set;
048import java.util.SortedSet;
049import java.util.StringTokenizer;
050
051import static com.puppycrawl.tools.checkstyle.Utils.fileExtensionMatches;
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{
061    /** maintains error count */
062    private final SeverityLevelCounter counter = new SeverityLevelCounter(
063            SeverityLevel.ERROR);
064
065    /** vector of listeners */
066    private final List<AuditListener> listeners = Lists.newArrayList();
067
068    /** vector of fileset checks */
069    private final List<FileSetCheck> fileSetChecks = Lists.newArrayList();
070
071    /** class loader to resolve classes with. **/
072    private ClassLoader loader = Thread.currentThread()
073            .getContextClassLoader();
074
075    /** the basedir to strip off in filenames */
076    private String basedir;
077
078    /** locale country to report messages  **/
079    private String localeCountry = Locale.getDefault().getCountry();
080    /** locale language to report messages  **/
081    private String localeLanguage = Locale.getDefault().getLanguage();
082
083    /** The factory for instantiating submodules */
084    private ModuleFactory moduleFactory;
085
086    /** The classloader used for loading Checkstyle module classes. */
087    private ClassLoader moduleClassLoader;
088
089    /** the context of all child components */
090    private Context childContext;
091
092    /** The audit event filters */
093    private final FilterSet filters = new FilterSet();
094
095    /** the file extensions that are accepted */
096    private String[] fileExtensions = {};
097
098    /**
099     * The severity level of any violations found by submodules.
100     * The value of this property is passed to submodules via
101     * contextualize().
102     *
103     * Note: Since the Checker is merely a container for modules
104     * it does not make sense to implement logging functionality
105     * here. Consequently Checker does not extend AbstractViolationReporter,
106     * leading to a bit of duplicated code for severity level setting.
107     */
108    private SeverityLevel severityLevel = SeverityLevel.ERROR;
109
110    /** Name of a charset */
111    private String charset = System.getProperty("file.encoding", "UTF-8");
112
113    /**
114     * Creates a new <code>Checker</code> instance.
115     * The instance needs to be contextualized and configured.
116     *
117     * @throws CheckstyleException if an error occurs
118     */
119    public Checker() throws CheckstyleException
120    {
121        addListener(counter);
122    }
123
124    @Override
125    public void finishLocalSetup() throws CheckstyleException
126    {
127        final Locale locale = new Locale(localeLanguage, localeCountry);
128        LocalizedMessage.setLocale(locale);
129
130        if (moduleFactory == null) {
131
132            if (moduleClassLoader == null) {
133                throw new CheckstyleException(
134                        "if no custom moduleFactory is set, "
135                                + "moduleClassLoader must be specified");
136            }
137
138            final Set<String> packageNames = PackageNamesLoader
139                    .getPackageNames(moduleClassLoader);
140            moduleFactory = new PackageObjectFactory(packageNames,
141                    moduleClassLoader);
142        }
143
144        final DefaultContext context = new DefaultContext();
145        context.add("charset", charset);
146        context.add("classLoader", loader);
147        context.add("moduleFactory", moduleFactory);
148        context.add("severity", severityLevel.getName());
149        context.add("basedir", basedir);
150        childContext = context;
151    }
152
153    @Override
154    protected void setupChild(Configuration childConf)
155        throws CheckstyleException
156    {
157        final String name = childConf.getName();
158        try {
159            final Object child = moduleFactory.createModule(name);
160            if (child instanceof AutomaticBean) {
161                final AutomaticBean bean = (AutomaticBean) child;
162                bean.contextualize(childContext);
163                bean.configure(childConf);
164            }
165            if (child instanceof FileSetCheck) {
166                final FileSetCheck fsc = (FileSetCheck) child;
167                addFileSetCheck(fsc);
168            }
169            else if (child instanceof Filter) {
170                final Filter filter = (Filter) child;
171                addFilter(filter);
172            }
173            else if (child instanceof AuditListener) {
174                final AuditListener listener = (AuditListener) child;
175                addListener(listener);
176            }
177            else {
178                throw new CheckstyleException(name
179                        + " is not allowed as a child in Checker");
180            }
181        }
182        catch (final Exception ex) {
183            // TODO i18n
184            throw new CheckstyleException("cannot initialize module " + name
185                    + " - " + ex.getMessage(), ex);
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    {
196        fileSetCheck.setMessageDispatcher(this);
197        fileSetChecks.add(fileSetCheck);
198    }
199
200    /**
201     * Adds a filter to the end of the audit event filter chain.
202     * @param filter the additional filter
203     */
204    public void addFilter(Filter filter)
205    {
206        filters.addFilter(filter);
207    }
208
209    /**
210     * Removes filter.
211     * @param filter filter to remove.
212     */
213    public void removeFilter(Filter filter)
214    {
215        filters.removeFilter(filter);
216    }
217
218    /** Cleans up the object. **/
219    public void destroy()
220    {
221        listeners.clear();
222        filters.clear();
223    }
224
225    /**
226     * Add the listener that will be used to receive events from the audit.
227     * @param listener the nosy thing
228     */
229    public final void addListener(AuditListener listener)
230    {
231        listeners.add(listener);
232    }
233
234    /**
235     * Removes a given listener.
236     * @param listener a listener to remove
237     */
238    public void removeListener(AuditListener listener)
239    {
240        listeners.remove(listener);
241    }
242
243    /**
244     * Processes a set of files with all FileSetChecks.
245     * Once this is done, it is highly recommended to call for
246     * the destroy method to close and remove the listeners.
247     * @param files the list of files to be audited.
248     * @return the total number of errors found
249     * @see #destroy()
250     */
251    public int process(List<File> files)
252    {
253        // Prepare to start
254        fireAuditStarted();
255        for (final FileSetCheck fsc : fileSetChecks) {
256            fsc.beginProcessing(charset);
257        }
258
259        // Process each file
260        for (final File f : files) {
261            if (!fileExtensionMatches(f, fileExtensions)) {
262                continue;
263            }
264            final String fileName = f.getAbsolutePath();
265            fireFileStarted(fileName);
266            final SortedSet<LocalizedMessage> fileMessages = Sets.newTreeSet();
267            try {
268                final FileText theText = new FileText(f.getAbsoluteFile(),
269                        charset);
270                for (final FileSetCheck fsc : fileSetChecks) {
271                    fileMessages.addAll(fsc.process(f, theText));
272                }
273            }
274            catch (final FileNotFoundException fnfe) {
275                Utils.getExceptionLogger().debug(
276                        "FileNotFoundException occured.", fnfe);
277                fileMessages.add(new LocalizedMessage(0,
278                        Defn.CHECKSTYLE_BUNDLE, "general.fileNotFound", null,
279                        null, this.getClass(), null));
280            }
281            catch (final IOException ioe) {
282                Utils.getExceptionLogger().debug("IOException occured.", ioe);
283                fileMessages.add(new LocalizedMessage(0,
284                        Defn.CHECKSTYLE_BUNDLE, "general.exception",
285                        new String[] {ioe.getMessage()}, null, this.getClass(),
286                        null));
287            }
288            fireErrors(fileName, fileMessages);
289            fireFileFinished(fileName);
290        }
291
292        // Finish up
293        for (final FileSetCheck fsc : fileSetChecks) {
294            // They may also log!!!
295            fsc.finishProcessing();
296            fsc.destroy();
297        }
298
299        final int errorCount = counter.getCount();
300        fireAuditFinished();
301        return errorCount;
302    }
303
304    /**
305     * Create a stripped down version of a filename.
306     * @param fileName the original filename
307     * @return the filename where an initial prefix of basedir is stripped
308     */
309    private String getStrippedFileName(final String fileName)
310    {
311        return Utils.getStrippedFileName(basedir, fileName);
312    }
313
314    /** @param basedir the base directory to strip off in filenames */
315    public void setBasedir(String basedir)
316    {
317        // we use getAbsolutePath() instead of getCanonicalPath()
318        // because normalize() removes all . and .. so path
319        // will be canonical by default.
320        this.basedir = normalize(basedir);
321    }
322
323    /**
324     * &quot;normalize&quot; the given absolute path.
325     *
326     * <p>This includes:
327     * <ul>
328     *   <li>Uppercase the drive letter if there is one.</li>
329     *   <li>Remove redundant slashes after the drive spec.</li>
330     *   <li>resolve all ./, .\, ../ and ..\ sequences.</li>
331     *   <li>DOS style paths that start with a drive letter will have
332     *     \ as the separator.</li>
333     * </ul>
334     * <p>
335     *
336     * @param normalizingPath a path for &quot;normalizing&quot;
337     * @return &quot;normalized&quot; file name
338     * @throws java.lang.NullPointerException if the file path is
339     * equal to null.
340     */
341    public String normalize(String normalizingPath)
342    {
343
344        if (normalizingPath == null) {
345            return normalizingPath;
346        }
347
348        final String osName = System.getProperty("os.name").toLowerCase(
349                Locale.US);
350        final boolean onNetWare = (osName.indexOf("netware") > -1);
351
352        String path = normalizingPath.replace('/', File.separatorChar).replace('\\',
353            File.separatorChar);
354
355        // make sure we are dealing with an absolute path
356        final int colon = path.indexOf(":");
357
358        if (!onNetWare) {
359            if (!path.startsWith(File.separator)
360                && !((path.length() >= 2)
361                     && Character.isLetter(path.charAt(0)) && (colon == 1)))
362            {
363                final String msg = path + " is not an absolute path";
364                throw new IllegalArgumentException(msg);
365            }
366        }
367        else {
368            if (!path.startsWith(File.separator) && (colon == -1)) {
369                final String msg = path + " is not an absolute path";
370                throw new IllegalArgumentException(msg);
371            }
372        }
373
374        boolean dosWithDrive = false;
375        String root = null;
376        // Eliminate consecutive slashes after the drive spec
377        if ((!onNetWare && (path.length() >= 2)
378             && Character.isLetter(path.charAt(0)) && (path.charAt(1) == ':'))
379            || (onNetWare && (colon > -1)))
380        {
381
382            dosWithDrive = true;
383
384            final char[] ca = path.replace('/', '\\').toCharArray();
385            final StringBuffer sbRoot = new StringBuffer();
386            for (int i = 0; i < colon; i++) {
387                sbRoot.append(Character.toUpperCase(ca[i]));
388            }
389            sbRoot.append(':');
390            if (colon + 1 < path.length()) {
391                sbRoot.append(File.separatorChar);
392            }
393            root = sbRoot.toString();
394
395            // Eliminate consecutive slashes after the drive spec
396            final StringBuffer sbPath = new StringBuffer();
397            for (int i = colon + 1; i < ca.length; i++) {
398                if ((ca[i] != '\\') || ((ca[i] == '\\') && (ca[i - 1] != '\\')))
399                {
400                    sbPath.append(ca[i]);
401                }
402            }
403            path = sbPath.toString().replace('\\', File.separatorChar);
404
405        }
406        else {
407            if (path.length() == 1) {
408                root = File.separator;
409                path = "";
410            }
411            else if (path.charAt(1) == File.separatorChar) {
412                // UNC drive
413                root = File.separator + File.separator;
414                path = path.substring(2);
415            }
416            else {
417                root = File.separator;
418                path = path.substring(1);
419            }
420        }
421
422        final FastStack<String> s = FastStack.newInstance();
423        s.push(root);
424        final StringTokenizer tok = new StringTokenizer(path, File.separator);
425        while (tok.hasMoreTokens()) {
426            final String thisToken = tok.nextToken();
427            if (".".equals(thisToken)) {
428                continue;
429            }
430            else if ("..".equals(thisToken)) {
431                if (s.size() < 2) {
432                    throw new IllegalArgumentException("Cannot resolve path "
433                            + path);
434                }
435                s.pop();
436            }
437            else { // plain component
438                s.push(thisToken);
439            }
440        }
441
442        final StringBuffer sb = new StringBuffer();
443        for (int i = 0; i < s.size(); i++) {
444            if (i > 1) {
445                // not before the filesystem root and not after it, since root
446                // already contains one
447                sb.append(File.separatorChar);
448            }
449            sb.append(s.peek(i));
450        }
451
452        path = sb.toString();
453        if (dosWithDrive) {
454            path = path.replace('/', '\\');
455        }
456        return path;
457    }
458
459    /** @return the base directory property used in unit-test. */
460    public final String getBasedir()
461    {
462        return basedir;
463    }
464
465    /** notify all listeners about the audit start */
466    protected void fireAuditStarted()
467    {
468        final AuditEvent evt = new AuditEvent(this);
469        for (final AuditListener listener : listeners) {
470            listener.auditStarted(evt);
471        }
472    }
473
474    /** notify all listeners about the audit end */
475    protected void fireAuditFinished()
476    {
477        final AuditEvent evt = new AuditEvent(this);
478        for (final AuditListener listener : listeners) {
479            listener.auditFinished(evt);
480        }
481    }
482
483    /**
484     * Notify all listeners about the beginning of a file audit.
485     *
486     * @param fileName
487     *            the file to be audited
488     */
489    @Override
490    public void fireFileStarted(String fileName)
491    {
492        final String stripped = getStrippedFileName(fileName);
493        final AuditEvent evt = new AuditEvent(this, stripped);
494        for (final AuditListener listener : listeners) {
495            listener.fileStarted(evt);
496        }
497    }
498
499    /**
500     * Notify all listeners about the end of a file audit.
501     *
502     * @param fileName
503     *            the audited file
504     */
505    @Override
506    public void fireFileFinished(String fileName)
507    {
508        final String stripped = getStrippedFileName(fileName);
509        final AuditEvent evt = new AuditEvent(this, stripped);
510        for (final AuditListener listener : listeners) {
511            listener.fileFinished(evt);
512        }
513    }
514
515    /**
516     * notify all listeners about the errors in a file.
517     *
518     * @param fileName the audited file
519     * @param errors the audit errors from the file
520     */
521    @Override
522    public void fireErrors(String fileName,
523        SortedSet<LocalizedMessage> errors)
524    {
525        final String stripped = getStrippedFileName(fileName);
526        for (final LocalizedMessage element : errors) {
527            final AuditEvent evt = new AuditEvent(this, stripped, element);
528            if (filters.accept(evt)) {
529                for (final AuditListener listener : listeners) {
530                    listener.addError(evt);
531                }
532            }
533        }
534    }
535
536    /**
537     * Sets the file extensions that identify the files that pass the
538     * filter of this FileSetCheck.
539     * @param extensions the set of file extensions. A missing
540     * initial '.' character of an extension is automatically added.
541     */
542    public final void setFileExtensions(String[] extensions)
543    {
544        if (extensions == null) {
545            fileExtensions = null;
546            return;
547        }
548
549        fileExtensions = new String[extensions.length];
550        for (int i = 0; i < extensions.length; i++) {
551            final String extension = extensions[i];
552            if (extension.startsWith(".")) {
553                fileExtensions[i] = extension;
554            }
555            else {
556                fileExtensions[i] = "." + extension;
557            }
558        }
559    }
560
561    /**
562     * Sets the factory for creating submodules.
563     *
564     * @param moduleFactory the factory for creating FileSetChecks
565     */
566    public void setModuleFactory(ModuleFactory moduleFactory)
567    {
568        this.moduleFactory = moduleFactory;
569    }
570
571    /** @param localeCountry the country to report messages  **/
572    public void setLocaleCountry(String localeCountry)
573    {
574        this.localeCountry = localeCountry;
575    }
576
577    /** @param localeLanguage the language to report messages  **/
578    public void setLocaleLanguage(String localeLanguage)
579    {
580        this.localeLanguage = localeLanguage;
581    }
582
583    /**
584     * Sets the severity level.  The string should be one of the names
585     * defined in the <code>SeverityLevel</code> class.
586     *
587     * @param severity  The new severity level
588     * @see SeverityLevel
589     */
590    public final void setSeverity(String severity)
591    {
592        severityLevel = SeverityLevel.getInstance(severity);
593    }
594
595    /**
596     * Sets the classloader that is used to contextualize filesetchecks.
597     * Some Check implementations will use that classloader to improve the
598     * quality of their reports, e.g. to load a class and then analyze it via
599     * reflection.
600     * @param loader the new classloader
601     */
602    public final void setClassloader(ClassLoader loader)
603    {
604        this.loader = loader;
605    }
606
607    /**
608     * Sets the classloader used to load Checkstyle core and custom module
609     * classes when the module tree is being built up.
610     * If no custom ModuleFactory is being set for the Checker module then
611     * this module classloader must be specified.
612     * @param moduleClassLoader the classloader used to load module classes
613     */
614    public final void setModuleClassLoader(ClassLoader moduleClassLoader)
615    {
616        this.moduleClassLoader = moduleClassLoader;
617    }
618
619    /**
620     * Sets a named charset.
621     * @param charset the name of a charset
622     * @throws UnsupportedEncodingException if charset is unsupported.
623     */
624    public void setCharset(String charset)
625        throws UnsupportedEncodingException
626    {
627        if (!Charset.isSupported(charset)) {
628            final String message = "unsupported charset: '" + charset + "'";
629            throw new UnsupportedEncodingException(message);
630        }
631        this.charset = charset;
632    }
633}