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