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