001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.PrintWriter; 025import java.io.StringWriter; 026import java.io.UnsupportedEncodingException; 027import java.nio.charset.Charset; 028import java.util.ArrayList; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Locale; 032import java.util.Set; 033import java.util.SortedSet; 034import java.util.TreeSet; 035 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038 039import com.puppycrawl.tools.checkstyle.api.AuditEvent; 040import com.puppycrawl.tools.checkstyle.api.AuditListener; 041import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 042import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter; 043import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet; 044import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 045import com.puppycrawl.tools.checkstyle.api.Configuration; 046import com.puppycrawl.tools.checkstyle.api.Context; 047import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder; 048import com.puppycrawl.tools.checkstyle.api.FileSetCheck; 049import com.puppycrawl.tools.checkstyle.api.FileText; 050import com.puppycrawl.tools.checkstyle.api.Filter; 051import com.puppycrawl.tools.checkstyle.api.FilterSet; 052import com.puppycrawl.tools.checkstyle.api.LocalizedMessage; 053import com.puppycrawl.tools.checkstyle.api.MessageDispatcher; 054import com.puppycrawl.tools.checkstyle.api.RootModule; 055import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 056import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter; 057import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 058 059/** 060 * This class provides the functionality to check a set of files. 061 * @author Oliver Burn 062 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a> 063 * @author lkuehne 064 * @author Andrei Selkin 065 */ 066public class Checker extends AutomaticBean implements MessageDispatcher, RootModule { 067 /** Message to use when an exception occurs and should be printed as a violation. */ 068 public static final String EXCEPTION_MSG = "general.exception"; 069 070 /** Logger for Checker. */ 071 private static final Log LOG = LogFactory.getLog(Checker.class); 072 073 /** Maintains error count. */ 074 private final SeverityLevelCounter counter = new SeverityLevelCounter( 075 SeverityLevel.ERROR); 076 077 /** Vector of listeners. */ 078 private final List<AuditListener> listeners = new ArrayList<>(); 079 080 /** Vector of fileset checks. */ 081 private final List<FileSetCheck> fileSetChecks = new ArrayList<>(); 082 083 /** The audit event before execution file filters. */ 084 private final BeforeExecutionFileFilterSet beforeExecutionFileFilters = 085 new BeforeExecutionFileFilterSet(); 086 087 /** The audit event filters. */ 088 private final FilterSet filters = new FilterSet(); 089 090 /** Class loader to resolve classes with. **/ 091 private ClassLoader classLoader = Thread.currentThread() 092 .getContextClassLoader(); 093 094 /** The basedir to strip off in file names. */ 095 private String basedir; 096 097 /** Locale country to report messages . **/ 098 private String localeCountry = Locale.getDefault().getCountry(); 099 /** Locale language to report messages . **/ 100 private String localeLanguage = Locale.getDefault().getLanguage(); 101 102 /** The factory for instantiating submodules. */ 103 private ModuleFactory moduleFactory; 104 105 /** The classloader used for loading Checkstyle module classes. */ 106 private ClassLoader moduleClassLoader; 107 108 /** The context of all child components. */ 109 private Context childContext; 110 111 /** The file extensions that are accepted. */ 112 private String[] fileExtensions = CommonUtils.EMPTY_STRING_ARRAY; 113 114 /** 115 * The severity level of any violations found by submodules. 116 * The value of this property is passed to submodules via 117 * contextualize(). 118 * 119 * <p>Note: Since the Checker is merely a container for modules 120 * it does not make sense to implement logging functionality 121 * here. Consequently Checker does not extend AbstractViolationReporter, 122 * leading to a bit of duplicated code for severity level setting. 123 */ 124 private SeverityLevel severityLevel = SeverityLevel.ERROR; 125 126 /** Name of a charset. */ 127 private String charset = System.getProperty("file.encoding", "UTF-8"); 128 129 /** Cache file. **/ 130 private PropertyCacheFile cache; 131 132 /** Controls whether exceptions should halt execution or not. */ 133 private boolean haltOnException = true; 134 135 /** 136 * Creates a new {@code Checker} instance. 137 * The instance needs to be contextualized and configured. 138 */ 139 public Checker() { 140 addListener(counter); 141 } 142 143 /** 144 * Sets cache file. 145 * @param fileName the cache file. 146 * @throws IOException if there are some problems with file loading. 147 */ 148 public void setCacheFile(String fileName) throws IOException { 149 final Configuration configuration = getConfiguration(); 150 cache = new PropertyCacheFile(configuration, fileName); 151 cache.load(); 152 } 153 154 /** 155 * Removes before execution file filter. 156 * @param filter before execution file filter to remove. 157 */ 158 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 159 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter); 160 } 161 162 /** 163 * Removes filter. 164 * @param filter filter to remove. 165 */ 166 public void removeFilter(Filter filter) { 167 filters.removeFilter(filter); 168 } 169 170 @Override 171 public void destroy() { 172 listeners.clear(); 173 beforeExecutionFileFilters.clear(); 174 filters.clear(); 175 if (cache != null) { 176 try { 177 cache.persist(); 178 } 179 catch (IOException ex) { 180 throw new IllegalStateException("Unable to persist cache file.", ex); 181 } 182 } 183 } 184 185 /** 186 * Removes a given listener. 187 * @param listener a listener to remove 188 */ 189 public void removeListener(AuditListener listener) { 190 listeners.remove(listener); 191 } 192 193 /** 194 * Sets base directory. 195 * @param basedir the base directory to strip off in file names 196 */ 197 public void setBasedir(String basedir) { 198 this.basedir = basedir; 199 } 200 201 @Override 202 public int process(List<File> files) throws CheckstyleException { 203 if (cache != null) { 204 cache.putExternalResources(getExternalResourceLocations()); 205 } 206 207 // Prepare to start 208 fireAuditStarted(); 209 for (final FileSetCheck fsc : fileSetChecks) { 210 fsc.beginProcessing(charset); 211 } 212 213 processFiles(files); 214 215 // Finish up 216 // It may also log!!! 217 fileSetChecks.forEach(FileSetCheck::finishProcessing); 218 219 // It may also log!!! 220 fileSetChecks.forEach(FileSetCheck::destroy); 221 222 final int errorCount = counter.getCount(); 223 fireAuditFinished(); 224 return errorCount; 225 } 226 227 /** 228 * Returns a set of external configuration resource locations which are used by all file set 229 * checks and filters. 230 * @return a set of external configuration resource locations which are used by all file set 231 * checks and filters. 232 */ 233 private Set<String> getExternalResourceLocations() { 234 final Set<String> externalResources = new HashSet<>(); 235 fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder) 236 .forEach(check -> { 237 final Set<String> locations = 238 ((ExternalResourceHolder) check).getExternalResourceLocations(); 239 externalResources.addAll(locations); 240 }); 241 filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder) 242 .forEach(filter -> { 243 final Set<String> locations = 244 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 245 externalResources.addAll(locations); 246 }); 247 return externalResources; 248 } 249 250 /** Notify all listeners about the audit start. */ 251 private void fireAuditStarted() { 252 final AuditEvent event = new AuditEvent(this); 253 for (final AuditListener listener : listeners) { 254 listener.auditStarted(event); 255 } 256 } 257 258 /** Notify all listeners about the audit end. */ 259 private void fireAuditFinished() { 260 final AuditEvent event = new AuditEvent(this); 261 for (final AuditListener listener : listeners) { 262 listener.auditFinished(event); 263 } 264 } 265 266 /** 267 * Processes a list of files with all FileSetChecks. 268 * @param files a list of files to process. 269 * @throws CheckstyleException if error condition within Checkstyle occurs. 270 * @noinspection ProhibitedExceptionThrown 271 */ 272 private void processFiles(List<File> files) throws CheckstyleException { 273 for (final File file : files) { 274 try { 275 final String fileName = file.getAbsolutePath(); 276 final long timestamp = file.lastModified(); 277 if (cache != null && cache.isInCache(fileName, timestamp) 278 || !CommonUtils.matchesFileExtension(file, fileExtensions) 279 || !acceptFileStarted(fileName)) { 280 continue; 281 } 282 if (cache != null) { 283 cache.put(fileName, timestamp); 284 } 285 fireFileStarted(fileName); 286 final SortedSet<LocalizedMessage> fileMessages = processFile(file); 287 fireErrors(fileName, fileMessages); 288 fireFileFinished(fileName); 289 } 290 // -@cs[IllegalCatch] There is no other way to deliver filename that was under 291 // processing. See https://github.com/checkstyle/checkstyle/issues/2285 292 catch (Exception ex) { 293 // We need to catch all exceptions to put a reason failure (file name) in exception 294 throw new CheckstyleException("Exception was thrown while processing " 295 + file.getPath(), ex); 296 } 297 catch (Error error) { 298 // We need to catch all errors to put a reason failure (file name) in error 299 throw new Error("Error was thrown while processing " + file.getPath(), error); 300 } 301 } 302 } 303 304 /** 305 * Processes a file with all FileSetChecks. 306 * @param file a file to process. 307 * @return a sorted set of messages to be logged. 308 * @throws CheckstyleException if error condition within Checkstyle occurs. 309 * @noinspection ProhibitedExceptionThrown 310 */ 311 private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException { 312 final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>(); 313 try { 314 final FileText theText = new FileText(file.getAbsoluteFile(), charset); 315 for (final FileSetCheck fsc : fileSetChecks) { 316 fileMessages.addAll(fsc.process(file, theText)); 317 } 318 } 319 catch (final IOException ioe) { 320 LOG.debug("IOException occurred.", ioe); 321 fileMessages.add(new LocalizedMessage(0, 322 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 323 new String[] {ioe.getMessage()}, null, getClass(), null)); 324 } 325 // -@cs[IllegalCatch] There is no other way to obey haltOnException field 326 catch (Exception ex) { 327 if (haltOnException) { 328 throw ex; 329 } 330 331 LOG.debug("Exception occurred.", ex); 332 333 final StringWriter sw = new StringWriter(); 334 final PrintWriter pw = new PrintWriter(sw, true); 335 336 ex.printStackTrace(pw); 337 338 fileMessages.add(new LocalizedMessage(0, 339 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 340 new String[] {sw.getBuffer().toString()}, 341 null, getClass(), null)); 342 } 343 return fileMessages; 344 } 345 346 /** 347 * Check if all before execution file filters accept starting the file. 348 * 349 * @param fileName 350 * the file to be audited 351 * @return {@code true} if the file is accepted. 352 */ 353 private boolean acceptFileStarted(String fileName) { 354 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 355 return beforeExecutionFileFilters.accept(stripped); 356 } 357 358 /** 359 * Notify all listeners about the beginning of a file audit. 360 * 361 * @param fileName 362 * the file to be audited 363 */ 364 @Override 365 public void fireFileStarted(String fileName) { 366 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 367 final AuditEvent event = new AuditEvent(this, stripped); 368 for (final AuditListener listener : listeners) { 369 listener.fileStarted(event); 370 } 371 } 372 373 /** 374 * Notify all listeners about the errors in a file. 375 * 376 * @param fileName the audited file 377 * @param errors the audit errors from the file 378 */ 379 @Override 380 public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) { 381 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 382 boolean hasNonFilteredViolations = false; 383 for (final LocalizedMessage element : errors) { 384 final AuditEvent event = new AuditEvent(this, stripped, element); 385 if (filters.accept(event)) { 386 hasNonFilteredViolations = true; 387 for (final AuditListener listener : listeners) { 388 listener.addError(event); 389 } 390 } 391 } 392 if (hasNonFilteredViolations && cache != null) { 393 cache.remove(fileName); 394 } 395 } 396 397 /** 398 * Notify all listeners about the end of a file audit. 399 * 400 * @param fileName 401 * the audited file 402 */ 403 @Override 404 public void fireFileFinished(String fileName) { 405 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 406 final AuditEvent event = new AuditEvent(this, stripped); 407 for (final AuditListener listener : listeners) { 408 listener.fileFinished(event); 409 } 410 } 411 412 @Override 413 public void finishLocalSetup() throws CheckstyleException { 414 final Locale locale = new Locale(localeLanguage, localeCountry); 415 LocalizedMessage.setLocale(locale); 416 417 if (moduleFactory == null) { 418 419 if (moduleClassLoader == null) { 420 throw new CheckstyleException( 421 "if no custom moduleFactory is set, " 422 + "moduleClassLoader must be specified"); 423 } 424 425 final Set<String> packageNames = PackageNamesLoader 426 .getPackageNames(moduleClassLoader); 427 moduleFactory = new PackageObjectFactory(packageNames, 428 moduleClassLoader); 429 } 430 431 final DefaultContext context = new DefaultContext(); 432 context.add("charset", charset); 433 context.add("classLoader", classLoader); 434 context.add("moduleFactory", moduleFactory); 435 context.add("severity", severityLevel.getName()); 436 context.add("basedir", basedir); 437 childContext = context; 438 } 439 440 /** 441 * {@inheritDoc} Creates child module. 442 * @noinspection ChainOfInstanceofChecks 443 */ 444 @Override 445 protected void setupChild(Configuration childConf) 446 throws CheckstyleException { 447 final String name = childConf.getName(); 448 final Object child; 449 450 try { 451 child = moduleFactory.createModule(name); 452 453 if (child instanceof AutomaticBean) { 454 final AutomaticBean bean = (AutomaticBean) child; 455 bean.contextualize(childContext); 456 bean.configure(childConf); 457 } 458 } 459 catch (final CheckstyleException ex) { 460 throw new CheckstyleException("cannot initialize module " + name 461 + " - " + ex.getMessage(), ex); 462 } 463 if (child instanceof FileSetCheck) { 464 final FileSetCheck fsc = (FileSetCheck) child; 465 fsc.init(); 466 addFileSetCheck(fsc); 467 } 468 else if (child instanceof BeforeExecutionFileFilter) { 469 final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child; 470 addBeforeExecutionFileFilter(filter); 471 } 472 else if (child instanceof Filter) { 473 final Filter filter = (Filter) child; 474 addFilter(filter); 475 } 476 else if (child instanceof AuditListener) { 477 final AuditListener listener = (AuditListener) child; 478 addListener(listener); 479 } 480 else { 481 throw new CheckstyleException(name 482 + " is not allowed as a child in Checker"); 483 } 484 } 485 486 /** 487 * Adds a FileSetCheck to the list of FileSetChecks 488 * that is executed in process(). 489 * @param fileSetCheck the additional FileSetCheck 490 */ 491 public void addFileSetCheck(FileSetCheck fileSetCheck) { 492 fileSetCheck.setMessageDispatcher(this); 493 fileSetChecks.add(fileSetCheck); 494 } 495 496 /** 497 * Adds a before execution file filter to the end of the event chain. 498 * @param filter the additional filter 499 */ 500 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 501 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 502 } 503 504 /** 505 * Adds a filter to the end of the audit event filter chain. 506 * @param filter the additional filter 507 */ 508 public void addFilter(Filter filter) { 509 filters.addFilter(filter); 510 } 511 512 @Override 513 public final void addListener(AuditListener listener) { 514 listeners.add(listener); 515 } 516 517 /** 518 * Sets the file extensions that identify the files that pass the 519 * filter of this FileSetCheck. 520 * @param extensions the set of file extensions. A missing 521 * initial '.' character of an extension is automatically added. 522 */ 523 public final void setFileExtensions(String... extensions) { 524 if (extensions == null) { 525 fileExtensions = null; 526 } 527 else { 528 fileExtensions = new String[extensions.length]; 529 for (int i = 0; i < extensions.length; i++) { 530 final String extension = extensions[i]; 531 if (CommonUtils.startsWithChar(extension, '.')) { 532 fileExtensions[i] = extension; 533 } 534 else { 535 fileExtensions[i] = "." + extension; 536 } 537 } 538 } 539 } 540 541 /** 542 * Sets the factory for creating submodules. 543 * 544 * @param moduleFactory the factory for creating FileSetChecks 545 */ 546 public void setModuleFactory(ModuleFactory moduleFactory) { 547 this.moduleFactory = moduleFactory; 548 } 549 550 /** 551 * Sets locale country. 552 * @param localeCountry the country to report messages 553 */ 554 public void setLocaleCountry(String localeCountry) { 555 this.localeCountry = localeCountry; 556 } 557 558 /** 559 * Sets locale language. 560 * @param localeLanguage the language to report messages 561 */ 562 public void setLocaleLanguage(String localeLanguage) { 563 this.localeLanguage = localeLanguage; 564 } 565 566 /** 567 * Sets the severity level. The string should be one of the names 568 * defined in the {@code SeverityLevel} class. 569 * 570 * @param severity The new severity level 571 * @see SeverityLevel 572 */ 573 public final void setSeverity(String severity) { 574 severityLevel = SeverityLevel.getInstance(severity); 575 } 576 577 /** 578 * Sets the classloader that is used to contextualize fileset checks. 579 * Some Check implementations will use that classloader to improve the 580 * quality of their reports, e.g. to load a class and then analyze it via 581 * reflection. 582 * @param classLoader the new classloader 583 */ 584 public final void setClassLoader(ClassLoader classLoader) { 585 this.classLoader = classLoader; 586 } 587 588 @Override 589 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 590 this.moduleClassLoader = moduleClassLoader; 591 } 592 593 /** 594 * Sets a named charset. 595 * @param charset the name of a charset 596 * @throws UnsupportedEncodingException if charset is unsupported. 597 */ 598 public void setCharset(String charset) 599 throws UnsupportedEncodingException { 600 if (!Charset.isSupported(charset)) { 601 final String message = "unsupported charset: '" + charset + "'"; 602 throw new UnsupportedEncodingException(message); 603 } 604 this.charset = charset; 605 } 606 607 /** 608 * Sets the field haltOnException. 609 * @param haltOnException the new value. 610 */ 611 public void setHaltOnException(boolean haltOnException) { 612 this.haltOnException = haltOnException; 613 } 614 615 /** 616 * Clears the cache. 617 */ 618 public void clearCache() { 619 if (cache != null) { 620 cache.reset(); 621 } 622 } 623}