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