001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2015 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.ant; 021 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FileOutputStream; 025import java.io.IOException; 026import java.io.OutputStream; 027import java.net.URL; 028import java.util.List; 029import java.util.Map; 030import java.util.Properties; 031import java.util.ResourceBundle; 032 033import org.apache.tools.ant.AntClassLoader; 034import org.apache.tools.ant.BuildException; 035import org.apache.tools.ant.DirectoryScanner; 036import org.apache.tools.ant.Project; 037import org.apache.tools.ant.Task; 038import org.apache.tools.ant.taskdefs.LogOutputStream; 039import org.apache.tools.ant.types.EnumeratedAttribute; 040import org.apache.tools.ant.types.FileSet; 041import org.apache.tools.ant.types.Path; 042import org.apache.tools.ant.types.Reference; 043 044import com.google.common.collect.Lists; 045import com.google.common.io.Closeables; 046import com.puppycrawl.tools.checkstyle.Checker; 047import com.puppycrawl.tools.checkstyle.ConfigurationLoader; 048import com.puppycrawl.tools.checkstyle.DefaultContext; 049import com.puppycrawl.tools.checkstyle.DefaultLogger; 050import com.puppycrawl.tools.checkstyle.PropertiesExpander; 051import com.puppycrawl.tools.checkstyle.XMLLogger; 052import com.puppycrawl.tools.checkstyle.api.AuditListener; 053import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 054import com.puppycrawl.tools.checkstyle.api.Configuration; 055import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 056import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter; 057 058/** 059 * An implementation of a ANT task for calling checkstyle. See the documentation 060 * of the task for usage. 061 * @author Oliver Burn 062 */ 063public class CheckstyleAntTask extends Task { 064 /** Poor man's enum for an xml formatter. */ 065 private static final String E_XML = "xml"; 066 /** Poor man's enum for an plain formatter. */ 067 private static final String E_PLAIN = "plain"; 068 069 /** Suffix for time string. */ 070 private static final String TIME_SUFFIX = " ms."; 071 072 /** Class path to locate class files. */ 073 private Path classpath; 074 075 /** Name of file to check. */ 076 private String fileName; 077 078 /** Config file containing configuration. */ 079 private String configLocation; 080 081 /** Whether to fail build on violations. */ 082 private boolean failOnViolation = true; 083 084 /** Property to set on violations. */ 085 private String failureProperty; 086 087 /** Contains the filesets to process. */ 088 private final List<FileSet> fileSets = Lists.newArrayList(); 089 090 /** Contains the formatters to log to. */ 091 private final List<Formatter> formatters = Lists.newArrayList(); 092 093 /** Contains the Properties to override. */ 094 private final List<Property> overrideProps = Lists.newArrayList(); 095 096 /** The name of the properties file. */ 097 private File properties; 098 099 /** The maximum number of errors that are tolerated. */ 100 private int maxErrors; 101 102 /** The maximum number of warnings that are tolerated. */ 103 private int maxWarnings = Integer.MAX_VALUE; 104 105 /** 106 * Whether to omit ignored modules - some modules may log tove 107 * their severity depending on their configuration (e.g. WriteTag) so 108 * need to be included 109 */ 110 private boolean omitIgnoredModules = true; 111 112 //////////////////////////////////////////////////////////////////////////// 113 // Setters for ANT specific attributes 114 //////////////////////////////////////////////////////////////////////////// 115 116 /** 117 * Tells this task to set the named property to "true" when there 118 * is a violation. 119 * @param propertyName the name of the property to set 120 * in the event of an failure. 121 */ 122 public void setFailureProperty(String propertyName) { 123 failureProperty = propertyName; 124 } 125 126 /** 127 * Sets flag - whether to fail if a violation is found. 128 * @param fail whether to fail if a violation is found 129 */ 130 public void setFailOnViolation(boolean fail) { 131 failOnViolation = fail; 132 } 133 134 /** 135 * Sets the maximum number of errors allowed. Default is 0. 136 * @param maxErrors the maximum number of errors allowed. 137 */ 138 public void setMaxErrors(int maxErrors) { 139 this.maxErrors = maxErrors; 140 } 141 142 /** 143 * Sets the maximum number of warnings allowed. Default is 144 * {@link Integer#MAX_VALUE}. 145 * @param maxWarnings the maximum number of warnings allowed. 146 */ 147 public void setMaxWarnings(int maxWarnings) { 148 this.maxWarnings = maxWarnings; 149 } 150 151 /** 152 * Adds set of files (nested fileset attribute). 153 * @param fS the file set to add 154 */ 155 public void addFileset(FileSet fS) { 156 fileSets.add(fS); 157 } 158 159 /** 160 * Add a formatter. 161 * @param formatter the formatter to add for logging. 162 */ 163 public void addFormatter(Formatter formatter) { 164 formatters.add(formatter); 165 } 166 167 /** 168 * Add an override property. 169 * @param property the property to add 170 */ 171 public void addProperty(Property property) { 172 overrideProps.add(property); 173 } 174 175 /** 176 * Set the class path. 177 * @param classpath the path to locate classes 178 */ 179 public void setClasspath(Path classpath) { 180 if (this.classpath == null) { 181 this.classpath = classpath; 182 } 183 else { 184 this.classpath.append(classpath); 185 } 186 } 187 188 /** 189 * Set the class path from a reference defined elsewhere. 190 * @param classpathRef the reference to an instance defining the classpath 191 */ 192 public void setClasspathRef(Reference classpathRef) { 193 createClasspath().setRefid(classpathRef); 194 } 195 196 /** 197 * Creates classpath. 198 * @return a created path for locating classes 199 */ 200 public Path createClasspath() { 201 if (classpath == null) { 202 classpath = new Path(getProject()); 203 } 204 return classpath.createPath(); 205 } 206 207 /** 208 * Sets file to be checked. 209 * @param file the file to be checked 210 */ 211 public void setFile(File file) { 212 fileName = file.getAbsolutePath(); 213 } 214 215 /** 216 * Sets configuration file. 217 * @param file the configuration file to use 218 */ 219 public void setConfig(File file) { 220 setConfigLocation(file.getAbsolutePath()); 221 } 222 223 /** 224 * Sets URL to the configuration. 225 * @param url the URL of the configuration to use 226 */ 227 public void setConfigURL(URL url) { 228 setConfigLocation(url.toExternalForm()); 229 } 230 231 /** 232 * Sets the location of the configuration. 233 * @param location the location, which is either a 234 */ 235 private void setConfigLocation(String location) { 236 if (configLocation != null) { 237 throw new BuildException("Attributes 'config' and 'configURL' " 238 + "must not be set at the same time"); 239 } 240 configLocation = location; 241 } 242 243 /** 244 * Sets flag - whether to omit ignored modules. 245 * @param omit whether to omit ignored modules 246 */ 247 public void setOmitIgnoredModules(boolean omit) { 248 omitIgnoredModules = omit; 249 } 250 251 //////////////////////////////////////////////////////////////////////////// 252 // Setters for Checker configuration attributes 253 //////////////////////////////////////////////////////////////////////////// 254 255 /** 256 * Sets a properties file for use instead 257 * of individually setting them. 258 * @param props the properties File to use 259 */ 260 public void setProperties(File props) { 261 properties = props; 262 } 263 264 //////////////////////////////////////////////////////////////////////////// 265 // The doers 266 //////////////////////////////////////////////////////////////////////////// 267 268 @Override 269 public void execute() { 270 final long startTime = System.currentTimeMillis(); 271 272 try { 273 // output version info in debug mode 274 final ResourceBundle compilationProperties = ResourceBundle 275 .getBundle("checkstylecompilation"); 276 final String version = compilationProperties 277 .getString("checkstyle.compile.version"); 278 final String compileTimestamp = compilationProperties 279 .getString("checkstyle.compile.timestamp"); 280 log("checkstyle version " + version, Project.MSG_VERBOSE); 281 log("compiled on " + compileTimestamp, Project.MSG_VERBOSE); 282 283 // Check for no arguments 284 if (fileName == null && fileSets.isEmpty()) { 285 throw new BuildException( 286 "Must specify at least one of 'file' or nested 'fileset'.", 287 getLocation()); 288 } 289 if (configLocation == null) { 290 throw new BuildException("Must specify 'config'.", getLocation()); 291 } 292 realExecute(version); 293 } 294 finally { 295 final long endTime = System.currentTimeMillis(); 296 log("Total execution took " + (endTime - startTime) + TIME_SUFFIX, 297 Project.MSG_VERBOSE); 298 } 299 } 300 301 /** 302 * Helper implementation to perform execution. 303 * @param checkstyleVersion Checkstyle compile version. 304 */ 305 private void realExecute(String checkstyleVersion) { 306 // Create the checker 307 Checker checker = null; 308 try { 309 checker = createChecker(); 310 311 // setup the listeners 312 final AuditListener[] listeners = getListeners(); 313 for (AuditListener element : listeners) { 314 checker.addListener(element); 315 } 316 final SeverityLevelCounter warningCounter = 317 new SeverityLevelCounter(SeverityLevel.WARNING); 318 checker.addListener(warningCounter); 319 320 processFiles(checker, warningCounter, checkstyleVersion); 321 } 322 finally { 323 if (checker != null) { 324 checker.destroy(); 325 } 326 } 327 } 328 329 /** 330 * Scans and processes files by means given checker. 331 * @param checker Checker to process files 332 * @param warningCounter Checker's counter of warnings 333 * @param checkstyleVersion Checkstyle compile version 334 */ 335 private void processFiles(Checker checker, final SeverityLevelCounter warningCounter, 336 final String checkstyleVersion) { 337 final long startTime = System.currentTimeMillis(); 338 final List<File> files = scanFileSets(); 339 final long endTime = System.currentTimeMillis(); 340 log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX, 341 Project.MSG_VERBOSE); 342 343 log("Running Checkstyle " + checkstyleVersion + " on " + files.size() 344 + " files", Project.MSG_INFO); 345 log("Using configuration " + configLocation, Project.MSG_VERBOSE); 346 347 int numErrs; 348 349 try { 350 final long processingStartTime = System.currentTimeMillis(); 351 numErrs = checker.process(files); 352 final long processingEndTime = System.currentTimeMillis(); 353 log("To process the files took " + (processingEndTime - processingStartTime) 354 + TIME_SUFFIX, Project.MSG_VERBOSE); 355 } 356 catch (CheckstyleException e) { 357 throw new BuildException("Unable to process files: " + files, e); 358 } 359 final int numWarnings = warningCounter.getCount(); 360 final boolean ok = numErrs <= maxErrors && numWarnings <= maxWarnings; 361 362 // Handle the return status 363 if (!ok) { 364 final String failureMsg = 365 "Got " + numErrs + " errors and " + numWarnings 366 + " warnings."; 367 if (failureProperty != null) { 368 getProject().setProperty(failureProperty, failureMsg); 369 } 370 371 if (failOnViolation) { 372 throw new BuildException(failureMsg, getLocation()); 373 } 374 } 375 } 376 377 /** 378 * Creates new instance of {@code Checker}. 379 * @return new instance of {@code Checker} 380 */ 381 private Checker createChecker() { 382 Checker checker; 383 try { 384 final Properties props = createOverridingProperties(); 385 final Configuration config = 386 ConfigurationLoader.loadConfiguration( 387 configLocation, 388 new PropertiesExpander(props), 389 omitIgnoredModules); 390 391 final DefaultContext context = new DefaultContext(); 392 final ClassLoader loader = new AntClassLoader(getProject(), 393 classpath); 394 context.add("classloader", loader); 395 396 final ClassLoader moduleClassLoader = 397 Checker.class.getClassLoader(); 398 context.add("moduleClassLoader", moduleClassLoader); 399 400 checker = new Checker(); 401 checker.contextualize(context); 402 checker.configure(config); 403 } 404 catch (final CheckstyleException e) { 405 throw new BuildException(String.format("Unable to create a Checker: " 406 + "configLocation {%s}, classpath {%s}.", configLocation, classpath), e); 407 } 408 return checker; 409 } 410 411 /** 412 * Create the Properties object based on the arguments specified 413 * to the ANT task. 414 * @return the properties for property expansion expansion 415 * @throws BuildException if an error occurs 416 */ 417 private Properties createOverridingProperties() { 418 final Properties retVal = new Properties(); 419 420 // Load the properties file if specified 421 if (properties != null) { 422 FileInputStream inStream = null; 423 try { 424 inStream = new FileInputStream(properties); 425 retVal.load(inStream); 426 } 427 catch (final IOException e) { 428 throw new BuildException("Error loading Properties file '" 429 + properties + "'", e, getLocation()); 430 } 431 finally { 432 Closeables.closeQuietly(inStream); 433 } 434 } 435 436 // override with Ant properties like ${basedir} 437 final Map<String, Object> antProps = getProject().getProperties(); 438 for (Map.Entry<String, Object> entry : antProps.entrySet()) { 439 final String value = String.valueOf(entry.getValue()); 440 retVal.setProperty(entry.getKey(), value); 441 } 442 443 // override with properties specified in subelements 444 for (Property p : overrideProps) { 445 retVal.setProperty(p.getKey(), p.getValue()); 446 } 447 448 return retVal; 449 } 450 451 /** 452 * Return the list of listeners set in this task. 453 * @return the list of listeners. 454 */ 455 private AuditListener[] getListeners() { 456 final int formatterCount = Math.max(1, formatters.size()); 457 458 final AuditListener[] listeners = new AuditListener[formatterCount]; 459 460 // formatters 461 try { 462 if (formatters.isEmpty()) { 463 final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG); 464 final OutputStream err = new LogOutputStream(this, Project.MSG_ERR); 465 listeners[0] = new DefaultLogger(debug, true, err, true, true); 466 } 467 else { 468 for (int i = 0; i < formatterCount; i++) { 469 final Formatter formatter = formatters.get(i); 470 listeners[i] = formatter.createListener(this); 471 } 472 } 473 } 474 catch (IOException e) { 475 throw new BuildException(String.format("Unable to create listeners: " 476 + "formatters {%s}.", formatters), e); 477 } 478 return listeners; 479 } 480 481 /** 482 * Returns the list of files (full path name) to process. 483 * @return the list of files included via the filesets. 484 */ 485 protected List<File> scanFileSets() { 486 final List<File> list = Lists.newArrayList(); 487 if (fileName != null) { 488 // oops we've got an additional one to process, don't 489 // forget it. No sweat, it's fully resolved via the setter. 490 log("Adding standalone file for audit", Project.MSG_VERBOSE); 491 list.add(new File(fileName)); 492 } 493 for (int i = 0; i < fileSets.size(); i++) { 494 final FileSet fs = fileSets.get(i); 495 final DirectoryScanner ds = fs.getDirectoryScanner(getProject()); 496 ds.scan(); 497 498 final String[] names = ds.getIncludedFiles(); 499 log(i + ") Adding " + names.length + " files from directory " 500 + ds.getBasedir(), Project.MSG_VERBOSE); 501 502 for (String element : names) { 503 final String pathname = ds.getBasedir() + File.separator 504 + element; 505 list.add(new File(pathname)); 506 } 507 } 508 509 return list; 510 } 511 512 /** 513 * Poor mans enumeration for the formatter types. 514 * @author Oliver Burn 515 */ 516 public static class FormatterType extends EnumeratedAttribute { 517 /** My possible values. */ 518 private static final String[] VALUES = {E_XML, E_PLAIN}; 519 520 @Override 521 public String[] getValues() { 522 return VALUES.clone(); 523 } 524 } 525 526 /** 527 * Details about a formatter to be used. 528 * @author Oliver Burn 529 */ 530 public static class Formatter { 531 /** The formatter type. */ 532 private FormatterType formatterType; 533 /** The file to output to. */ 534 private File toFile; 535 /** Whether or not the write to the named file. */ 536 private boolean useFile = true; 537 538 /** 539 * Set the type of the formatter. 540 * @param type the type 541 */ 542 public void setType(FormatterType type) { 543 final String val = type.getValue(); 544 if (!E_XML.equals(val) && !E_PLAIN.equals(val)) { 545 throw new BuildException("Invalid formatter type: " + val); 546 } 547 548 formatterType = type; 549 } 550 551 /** 552 * Set the file to output to. 553 * @param to the file to output to 554 */ 555 public void setTofile(File to) { 556 toFile = to; 557 } 558 559 /** 560 * Sets whether or not we write to a file if it is provided. 561 * @param use whether not not to use provided file. 562 */ 563 public void setUseFile(boolean use) { 564 useFile = use; 565 } 566 567 /** 568 * Creates a listener for the formatter. 569 * @param task the task running 570 * @return a listener 571 * @throws IOException if an error occurs 572 */ 573 public AuditListener createListener(Task task) throws IOException { 574 if (formatterType != null 575 && E_XML.equals(formatterType.getValue())) { 576 return createXMLLogger(task); 577 } 578 return createDefaultLogger(task); 579 } 580 581 /** 582 * Creates default logger. 583 * @param task the task to possibly log to 584 * @return a DefaultLogger instance 585 * @throws IOException if an error occurs 586 */ 587 private AuditListener createDefaultLogger(Task task) 588 throws IOException { 589 if (toFile == null || !useFile) { 590 return new DefaultLogger( 591 new LogOutputStream(task, Project.MSG_DEBUG), 592 true, new LogOutputStream(task, Project.MSG_ERR), true); 593 } 594 final FileOutputStream infoStream = new FileOutputStream(toFile); 595 return new DefaultLogger(infoStream, true, infoStream, false, true); 596 } 597 598 /** 599 * Creates XML logger. 600 * @param task the task to possibly log to 601 * @return an XMLLogger instance 602 * @throws IOException if an error occurs 603 */ 604 private AuditListener createXMLLogger(Task task) throws IOException { 605 if (toFile == null || !useFile) { 606 return new XMLLogger(new LogOutputStream(task, 607 Project.MSG_INFO), true); 608 } 609 return new XMLLogger(new FileOutputStream(toFile), true); 610 } 611 } 612 613 /** 614 * Represents a property that consists of a key and value. 615 */ 616 public static class Property { 617 /** The property key. */ 618 private String key; 619 /** The property value. */ 620 private String value; 621 622 /** 623 * Gets key. 624 * @return the property key 625 */ 626 public String getKey() { 627 return key; 628 } 629 630 /** 631 * Sets key. 632 * @param key sets the property key 633 */ 634 public void setKey(String key) { 635 this.key = key; 636 } 637 638 /** 639 * Gets value. 640 * @return the property value 641 */ 642 public String getValue() { 643 return value; 644 } 645 646 /** 647 * Sets value. 648 * @param value set the property value 649 */ 650 public void setValue(String value) { 651 this.value = value; 652 } 653 654 /** 655 * Sets the property value from a File. 656 * @param file set the property value from a File 657 */ 658 public void setFile(File file) { 659 value = file.getAbsolutePath(); 660 } 661 } 662 663 /** Represents a custom listener. */ 664 public static class Listener { 665 /** Class name of the listener class. */ 666 private String className; 667 668 /** 669 * Gets class name. 670 * @return the class name 671 */ 672 public String getClassname() { 673 return className; 674 } 675 676 /** 677 * Sets class name. 678 * @param name set the class name 679 */ 680 public void setClassname(String name) { 681 className = name; 682 } 683 } 684}