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