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 051/** 052 * This class provides the functionality to check a set of files. 053 * @author Oliver Burn 054 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a> 055 * @author lkuehne 056 */ 057public class Checker extends AutomaticBean implements MessageDispatcher 058{ 059 /** maintains error count */ 060 private final SeverityLevelCounter mCounter = new SeverityLevelCounter( 061 SeverityLevel.ERROR); 062 063 /** vector of listeners */ 064 private final List<AuditListener> mListeners = Lists.newArrayList(); 065 066 /** vector of fileset checks */ 067 private final List<FileSetCheck> mFileSetChecks = Lists.newArrayList(); 068 069 /** class loader to resolve classes with. **/ 070 private ClassLoader mLoader = Thread.currentThread() 071 .getContextClassLoader(); 072 073 /** the basedir to strip off in filenames */ 074 private String mBasedir; 075 076 /** locale country to report messages **/ 077 private String mLocaleCountry = Locale.getDefault().getCountry(); 078 /** locale language to report messages **/ 079 private String mLocaleLanguage = Locale.getDefault().getLanguage(); 080 081 /** The factory for instantiating submodules */ 082 private ModuleFactory mModuleFactory; 083 084 /** The classloader used for loading Checkstyle module classes. */ 085 private ClassLoader mModuleClassLoader; 086 087 /** the context of all child components */ 088 private Context mChildContext; 089 090 /** The audit event filters */ 091 private final FilterSet mFilters = new FilterSet(); 092 093 /** 094 * The severity level of any violations found by submodules. 095 * The value of this property is passed to submodules via 096 * contextualize(). 097 * 098 * Note: Since the Checker is merely a container for modules 099 * it does not make sense to implement logging functionality 100 * here. Consequently Checker does not extend AbstractViolationReporter, 101 * leading to a bit of duplicated code for severity level setting. 102 */ 103 private SeverityLevel mSeverityLevel = SeverityLevel.ERROR; 104 105 /** Name of a charset */ 106 private String mCharset = System.getProperty("file.encoding", "UTF-8"); 107 108 /** 109 * Creates a new <code>Checker</code> instance. 110 * The instance needs to be contextualized and configured. 111 * 112 * @throws CheckstyleException if an error occurs 113 */ 114 public Checker() throws CheckstyleException 115 { 116 addListener(mCounter); 117 } 118 119 @Override 120 public void finishLocalSetup() throws CheckstyleException 121 { 122 final Locale locale = new Locale(mLocaleLanguage, mLocaleCountry); 123 LocalizedMessage.setLocale(locale); 124 125 if (mModuleFactory == null) { 126 127 if (mModuleClassLoader == null) { 128 throw new CheckstyleException( 129 "if no custom moduleFactory is set, " 130 + "moduleClassLoader must be specified"); 131 } 132 133 final Set<String> packageNames = PackageNamesLoader 134 .getPackageNames(mModuleClassLoader); 135 mModuleFactory = new PackageObjectFactory(packageNames, 136 mModuleClassLoader); 137 } 138 139 final DefaultContext context = new DefaultContext(); 140 context.add("charset", mCharset); 141 context.add("classLoader", mLoader); 142 context.add("moduleFactory", mModuleFactory); 143 context.add("severity", mSeverityLevel.getName()); 144 context.add("basedir", mBasedir); 145 mChildContext = context; 146 } 147 148 @Override 149 protected void setupChild(Configuration aChildConf) 150 throws CheckstyleException 151 { 152 final String name = aChildConf.getName(); 153 try { 154 final Object child = mModuleFactory.createModule(name); 155 if (child instanceof AutomaticBean) { 156 final AutomaticBean bean = (AutomaticBean) child; 157 bean.contextualize(mChildContext); 158 bean.configure(aChildConf); 159 } 160 if (child instanceof FileSetCheck) { 161 final FileSetCheck fsc = (FileSetCheck) child; 162 addFileSetCheck(fsc); 163 } 164 else if (child instanceof Filter) { 165 final Filter filter = (Filter) child; 166 addFilter(filter); 167 } 168 else if (child instanceof AuditListener) { 169 final AuditListener listener = (AuditListener) child; 170 addListener(listener); 171 } 172 else { 173 throw new CheckstyleException(name 174 + " is not allowed as a child in Checker"); 175 } 176 } 177 catch (final Exception ex) { 178 // TODO i18n 179 throw new CheckstyleException("cannot initialize module " + name 180 + " - " + ex.getMessage(), ex); 181 } 182 } 183 184 /** 185 * Adds a FileSetCheck to the list of FileSetChecks 186 * that is executed in process(). 187 * @param aFileSetCheck the additional FileSetCheck 188 */ 189 public void addFileSetCheck(FileSetCheck aFileSetCheck) 190 { 191 aFileSetCheck.setMessageDispatcher(this); 192 mFileSetChecks.add(aFileSetCheck); 193 } 194 195 /** 196 * Adds a filter to the end of the audit event filter chain. 197 * @param aFilter the additional filter 198 */ 199 public void addFilter(Filter aFilter) 200 { 201 mFilters.addFilter(aFilter); 202 } 203 204 /** 205 * Removes filter. 206 * @param aFilter filter to remove. 207 */ 208 public void removeFilter(Filter aFilter) 209 { 210 mFilters.removeFilter(aFilter); 211 } 212 213 /** Cleans up the object. **/ 214 public void destroy() 215 { 216 mListeners.clear(); 217 mFilters.clear(); 218 } 219 220 /** 221 * Add the listener that will be used to receive events from the audit. 222 * @param aListener the nosy thing 223 */ 224 public final void addListener(AuditListener aListener) 225 { 226 mListeners.add(aListener); 227 } 228 229 /** 230 * Removes a given listener. 231 * @param aListener a listener to remove 232 */ 233 public void removeListener(AuditListener aListener) 234 { 235 mListeners.remove(aListener); 236 } 237 238 /** 239 * Processes a set of files with all FileSetChecks. 240 * Once this is done, it is highly recommended to call for 241 * the destroy method to close and remove the listeners. 242 * @param aFiles the list of files to be audited. 243 * @return the total number of errors found 244 * @see #destroy() 245 */ 246 public int process(List<File> aFiles) 247 { 248 // Prepare to start 249 fireAuditStarted(); 250 for (final FileSetCheck fsc : mFileSetChecks) { 251 fsc.beginProcessing(mCharset); 252 } 253 254 // Process each file 255 for (final File f : aFiles) { 256 final String fileName = f.getAbsolutePath(); 257 fireFileStarted(fileName); 258 final SortedSet<LocalizedMessage> fileMessages = Sets.newTreeSet(); 259 try { 260 final FileText theText = new FileText(f.getAbsoluteFile(), 261 mCharset); 262 for (final FileSetCheck fsc : mFileSetChecks) { 263 fileMessages.addAll(fsc.process(f, theText)); 264 } 265 } 266 catch (final FileNotFoundException fnfe) { 267 Utils.getExceptionLogger().debug( 268 "FileNotFoundException occured.", fnfe); 269 fileMessages.add(new LocalizedMessage(0, 270 Defn.CHECKSTYLE_BUNDLE, "general.fileNotFound", null, 271 null, this.getClass(), null)); 272 } 273 catch (final IOException ioe) { 274 Utils.getExceptionLogger().debug("IOException occured.", ioe); 275 fileMessages.add(new LocalizedMessage(0, 276 Defn.CHECKSTYLE_BUNDLE, "general.exception", 277 new String[] {ioe.getMessage()}, null, this.getClass(), 278 null)); 279 } 280 fireErrors(fileName, fileMessages); 281 fireFileFinished(fileName); 282 } 283 284 // Finish up 285 for (final FileSetCheck fsc : mFileSetChecks) { 286 // They may also log!!! 287 fsc.finishProcessing(); 288 fsc.destroy(); 289 } 290 291 final int errorCount = mCounter.getCount(); 292 fireAuditFinished(); 293 return errorCount; 294 } 295 296 /** 297 * Create a stripped down version of a filename. 298 * @param aFileName the original filename 299 * @return the filename where an initial prefix of basedir is stripped 300 */ 301 private String getStrippedFileName(final String aFileName) 302 { 303 return Utils.getStrippedFileName(mBasedir, aFileName); 304 } 305 306 /** @param aBasedir the base directory to strip off in filenames */ 307 public void setBasedir(String aBasedir) 308 { 309 // we use getAbsolutePath() instead of getCanonicalPath() 310 // because normalize() removes all . and .. so path 311 // will be canonical by default. 312 mBasedir = normalize(aBasedir); 313 } 314 315 /** 316 * "normalize" the given absolute path. 317 * 318 * <p>This includes: 319 * <ul> 320 * <li>Uppercase the drive letter if there is one.</li> 321 * <li>Remove redundant slashes after the drive spec.</li> 322 * <li>resolve all ./, .\, ../ and ..\ sequences.</li> 323 * <li>DOS style paths that start with a drive letter will have 324 * \ as the separator.</li> 325 * </ul> 326 * <p> 327 * 328 * @param aPath a path for "normalizing" 329 * @return "normalized" file name 330 * @throws java.lang.NullPointerException if the file path is 331 * equal to null. 332 */ 333 public String normalize(String aPath) 334 { 335 final String osName = System.getProperty("os.name").toLowerCase( 336 Locale.US); 337 final boolean onNetWare = (osName.indexOf("netware") > -1); 338 339 String path = aPath.replace('/', File.separatorChar).replace('\\', 340 File.separatorChar); 341 342 // make sure we are dealing with an absolute path 343 final int colon = path.indexOf(":"); 344 345 if (!onNetWare) { 346 if (!path.startsWith(File.separator) 347 && !((path.length() >= 2) 348 && Character.isLetter(path.charAt(0)) && (colon == 1))) 349 { 350 final String msg = path + " is not an absolute path"; 351 throw new IllegalArgumentException(msg); 352 } 353 } 354 else { 355 if (!path.startsWith(File.separator) && (colon == -1)) { 356 final String msg = path + " is not an absolute path"; 357 throw new IllegalArgumentException(msg); 358 } 359 } 360 361 boolean dosWithDrive = false; 362 String root = null; 363 // Eliminate consecutive slashes after the drive spec 364 if ((!onNetWare && (path.length() >= 2) 365 && Character.isLetter(path.charAt(0)) && (path.charAt(1) == ':')) 366 || (onNetWare && (colon > -1))) 367 { 368 369 dosWithDrive = true; 370 371 final char[] ca = path.replace('/', '\\').toCharArray(); 372 final StringBuffer sbRoot = new StringBuffer(); 373 for (int i = 0; i < colon; i++) { 374 sbRoot.append(Character.toUpperCase(ca[i])); 375 } 376 sbRoot.append(':'); 377 if (colon + 1 < path.length()) { 378 sbRoot.append(File.separatorChar); 379 } 380 root = sbRoot.toString(); 381 382 // Eliminate consecutive slashes after the drive spec 383 final StringBuffer sbPath = new StringBuffer(); 384 for (int i = colon + 1; i < ca.length; i++) { 385 if ((ca[i] != '\\') || ((ca[i] == '\\') && (ca[i - 1] != '\\'))) 386 { 387 sbPath.append(ca[i]); 388 } 389 } 390 path = sbPath.toString().replace('\\', File.separatorChar); 391 392 } 393 else { 394 if (path.length() == 1) { 395 root = File.separator; 396 path = ""; 397 } 398 else if (path.charAt(1) == File.separatorChar) { 399 // UNC drive 400 root = File.separator + File.separator; 401 path = path.substring(2); 402 } 403 else { 404 root = File.separator; 405 path = path.substring(1); 406 } 407 } 408 409 final FastStack<String> s = FastStack.newInstance(); 410 s.push(root); 411 final StringTokenizer tok = new StringTokenizer(path, File.separator); 412 while (tok.hasMoreTokens()) { 413 final String thisToken = tok.nextToken(); 414 if (".".equals(thisToken)) { 415 continue; 416 } 417 else if ("..".equals(thisToken)) { 418 if (s.size() < 2) { 419 throw new IllegalArgumentException("Cannot resolve path " 420 + aPath); 421 } 422 s.pop(); 423 } 424 else { // plain component 425 s.push(thisToken); 426 } 427 } 428 429 final StringBuffer sb = new StringBuffer(); 430 for (int i = 0; i < s.size(); i++) { 431 if (i > 1) { 432 // not before the filesystem root and not after it, since root 433 // already contains one 434 sb.append(File.separatorChar); 435 } 436 sb.append(s.peek(i)); 437 } 438 439 path = sb.toString(); 440 if (dosWithDrive) { 441 path = path.replace('/', '\\'); 442 } 443 return path; 444 } 445 446 /** @return the base directory property used in unit-test. */ 447 public final String getBasedir() 448 { 449 return mBasedir; 450 } 451 452 /** notify all listeners about the audit start */ 453 protected void fireAuditStarted() 454 { 455 final AuditEvent evt = new AuditEvent(this); 456 for (final AuditListener listener : mListeners) { 457 listener.auditStarted(evt); 458 } 459 } 460 461 /** notify all listeners about the audit end */ 462 protected void fireAuditFinished() 463 { 464 final AuditEvent evt = new AuditEvent(this); 465 for (final AuditListener listener : mListeners) { 466 listener.auditFinished(evt); 467 } 468 } 469 470 /** 471 * Notify all listeners about the beginning of a file audit. 472 * 473 * @param aFileName 474 * the file to be audited 475 */ 476 public void fireFileStarted(String aFileName) 477 { 478 final String stripped = getStrippedFileName(aFileName); 479 final AuditEvent evt = new AuditEvent(this, stripped); 480 for (final AuditListener listener : mListeners) { 481 listener.fileStarted(evt); 482 } 483 } 484 485 /** 486 * Notify all listeners about the end of a file audit. 487 * 488 * @param aFileName 489 * the audited file 490 */ 491 public void fireFileFinished(String aFileName) 492 { 493 final String stripped = getStrippedFileName(aFileName); 494 final AuditEvent evt = new AuditEvent(this, stripped); 495 for (final AuditListener listener : mListeners) { 496 listener.fileFinished(evt); 497 } 498 } 499 500 /** 501 * notify all listeners about the errors in a file. 502 * 503 * @param aFileName the audited file 504 * @param aErrors the audit errors from the file 505 */ 506 public void fireErrors(String aFileName, 507 SortedSet<LocalizedMessage> aErrors) 508 { 509 final String stripped = getStrippedFileName(aFileName); 510 for (final LocalizedMessage element : aErrors) { 511 final AuditEvent evt = new AuditEvent(this, stripped, element); 512 if (mFilters.accept(evt)) { 513 for (final AuditListener listener : mListeners) { 514 listener.addError(evt); 515 } 516 } 517 } 518 } 519 520 /** 521 * Sets the factory for creating submodules. 522 * 523 * @param aModuleFactory the factory for creating FileSetChecks 524 */ 525 public void setModuleFactory(ModuleFactory aModuleFactory) 526 { 527 mModuleFactory = aModuleFactory; 528 } 529 530 /** @param aLocaleCountry the country to report messages **/ 531 public void setLocaleCountry(String aLocaleCountry) 532 { 533 mLocaleCountry = aLocaleCountry; 534 } 535 536 /** @param aLocaleLanguage the language to report messages **/ 537 public void setLocaleLanguage(String aLocaleLanguage) 538 { 539 mLocaleLanguage = aLocaleLanguage; 540 } 541 542 /** 543 * Sets the severity level. The string should be one of the names 544 * defined in the <code>SeverityLevel</code> class. 545 * 546 * @param aSeverity The new severity level 547 * @see SeverityLevel 548 */ 549 public final void setSeverity(String aSeverity) 550 { 551 mSeverityLevel = SeverityLevel.getInstance(aSeverity); 552 } 553 554 /** 555 * Sets the classloader that is used to contextualize filesetchecks. 556 * Some Check implementations will use that classloader to improve the 557 * quality of their reports, e.g. to load a class and then analyze it via 558 * reflection. 559 * @param aLoader the new classloader 560 */ 561 public final void setClassloader(ClassLoader aLoader) 562 { 563 mLoader = aLoader; 564 } 565 566 /** 567 * Sets the classloader used to load Checkstyle core and custom module 568 * classes when the module tree is being built up. 569 * If no custom ModuleFactory is being set for the Checker module then 570 * this module classloader must be specified. 571 * @param aModuleClassLoader the classloader used to load module classes 572 */ 573 public final void setModuleClassLoader(ClassLoader aModuleClassLoader) 574 { 575 mModuleClassLoader = aModuleClassLoader; 576 } 577 578 /** 579 * Sets a named charset. 580 * @param aCharset the name of a charset 581 * @throws UnsupportedEncodingException if aCharset is unsupported. 582 */ 583 public void setCharset(String aCharset) 584 throws UnsupportedEncodingException 585 { 586 if (!Charset.isSupported(aCharset)) { 587 final String message = "unsupported charset: '" + aCharset + "'"; 588 throw new UnsupportedEncodingException(message); 589 } 590 mCharset = aCharset; 591 } 592}