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