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 * "normalize" 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 "normalizing" 337 * @return "normalized" 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}