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