View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2014  Oliver Burn
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  package com.puppycrawl.tools.checkstyle;
20  
21  import com.google.common.collect.Lists;
22  import com.google.common.collect.Sets;
23  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
24  import com.puppycrawl.tools.checkstyle.api.AuditListener;
25  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
26  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
27  import com.puppycrawl.tools.checkstyle.api.Configuration;
28  import com.puppycrawl.tools.checkstyle.api.Context;
29  import com.puppycrawl.tools.checkstyle.api.FastStack;
30  import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
31  import com.puppycrawl.tools.checkstyle.api.FileText;
32  import com.puppycrawl.tools.checkstyle.api.Filter;
33  import com.puppycrawl.tools.checkstyle.api.FilterSet;
34  import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
35  import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
36  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
37  import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
38  import com.puppycrawl.tools.checkstyle.api.Utils;
39  
40  import java.io.File;
41  import java.io.FileNotFoundException;
42  import java.io.IOException;
43  import java.io.UnsupportedEncodingException;
44  import java.nio.charset.Charset;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Set;
48  import java.util.SortedSet;
49  import java.util.StringTokenizer;
50  
51  import static com.puppycrawl.tools.checkstyle.Utils.fileExtensionMatches;
52  
53  /**
54   * This class provides the functionality to check a set of files.
55   * @author Oliver Burn
56   * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
57   * @author lkuehne
58   */
59  public class Checker extends AutomaticBean implements MessageDispatcher
60  {
61      /** maintains error count */
62      private final SeverityLevelCounter counter = new SeverityLevelCounter(
63              SeverityLevel.ERROR);
64  
65      /** vector of listeners */
66      private final List<AuditListener> listeners = Lists.newArrayList();
67  
68      /** vector of fileset checks */
69      private final List<FileSetCheck> fileSetChecks = Lists.newArrayList();
70  
71      /** class loader to resolve classes with. **/
72      private ClassLoader loader = Thread.currentThread()
73              .getContextClassLoader();
74  
75      /** the basedir to strip off in filenames */
76      private String basedir;
77  
78      /** locale country to report messages  **/
79      private String localeCountry = Locale.getDefault().getCountry();
80      /** locale language to report messages  **/
81      private String localeLanguage = Locale.getDefault().getLanguage();
82  
83      /** The factory for instantiating submodules */
84      private ModuleFactory moduleFactory;
85  
86      /** The classloader used for loading Checkstyle module classes. */
87      private ClassLoader moduleClassLoader;
88  
89      /** the context of all child components */
90      private Context childContext;
91  
92      /** The audit event filters */
93      private final FilterSet filters = new FilterSet();
94  
95      /** the file extensions that are accepted */
96      private String[] fileExtensions = {};
97  
98      /**
99       * 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 }