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 * @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 * @throws BuildException when config was already set 234 */ 235 public void setConfig(String configuration) { 236 if (config != null) { 237 throw new BuildException("Attribute 'config' has already been set"); 238 } 239 config = configuration; 240 } 241 242 /** 243 * Sets flag - whether to execute ignored modules. 244 * @param omit whether to execute ignored modules 245 */ 246 public void setExecuteIgnoredModules(boolean omit) { 247 executeIgnoredModules = omit; 248 } 249 250 //////////////////////////////////////////////////////////////////////////// 251 // Setters for Root Module's configuration attributes 252 //////////////////////////////////////////////////////////////////////////// 253 254 /** 255 * Sets a properties file for use instead 256 * of individually setting them. 257 * @param props the properties File to use 258 */ 259 public void setProperties(File props) { 260 properties = props; 261 } 262 263 //////////////////////////////////////////////////////////////////////////// 264 // The doers 265 //////////////////////////////////////////////////////////////////////////// 266 267 @Override 268 public void execute() { 269 final long startTime = System.currentTimeMillis(); 270 271 try { 272 final String version = CheckstyleAntTask.class.getPackage().getImplementationVersion(); 273 274 log("checkstyle version " + version, Project.MSG_VERBOSE); 275 276 // Check for no arguments 277 if (fileName == null 278 && fileSets.isEmpty() 279 && paths.isEmpty()) { 280 throw new BuildException( 281 "Must specify at least one of 'file' or nested 'fileset' or 'path'.", 282 getLocation()); 283 } 284 if (config == null) { 285 throw new BuildException("Must specify 'config'.", getLocation()); 286 } 287 realExecute(version); 288 } 289 finally { 290 final long endTime = System.currentTimeMillis(); 291 log("Total execution took " + (endTime - startTime) + TIME_SUFFIX, 292 Project.MSG_VERBOSE); 293 } 294 } 295 296 /** 297 * Helper implementation to perform execution. 298 * @param checkstyleVersion Checkstyle compile version. 299 */ 300 private void realExecute(String checkstyleVersion) { 301 // Create the root module 302 RootModule rootModule = null; 303 try { 304 rootModule = createRootModule(); 305 306 // setup the listeners 307 final AuditListener[] listeners = getListeners(); 308 for (AuditListener element : listeners) { 309 rootModule.addListener(element); 310 } 311 final SeverityLevelCounter warningCounter = 312 new SeverityLevelCounter(SeverityLevel.WARNING); 313 rootModule.addListener(warningCounter); 314 315 processFiles(rootModule, warningCounter, checkstyleVersion); 316 } 317 finally { 318 if (rootModule != null) { 319 rootModule.destroy(); 320 } 321 } 322 } 323 324 /** 325 * Scans and processes files by means given root module. 326 * @param rootModule Root module to process files 327 * @param warningCounter Root Module's counter of warnings 328 * @param checkstyleVersion Checkstyle compile version 329 */ 330 private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter, 331 final String checkstyleVersion) { 332 final long startTime = System.currentTimeMillis(); 333 final List<File> files = getFilesToCheck(); 334 final long endTime = System.currentTimeMillis(); 335 log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX, 336 Project.MSG_VERBOSE); 337 338 log("Running Checkstyle " 339 + Objects.toString(checkstyleVersion, "") 340 + " on " + files.size() 341 + " files", Project.MSG_INFO); 342 log("Using configuration " + config, Project.MSG_VERBOSE); 343 344 final int numErrs; 345 346 try { 347 final long processingStartTime = System.currentTimeMillis(); 348 numErrs = rootModule.process(files); 349 final long processingEndTime = System.currentTimeMillis(); 350 log("To process the files took " + (processingEndTime - processingStartTime) 351 + TIME_SUFFIX, Project.MSG_VERBOSE); 352 } 353 catch (CheckstyleException ex) { 354 throw new BuildException("Unable to process files: " + files, ex); 355 } 356 final int numWarnings = warningCounter.getCount(); 357 final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings; 358 359 // Handle the return status 360 if (!okStatus) { 361 final String failureMsg = 362 "Got " + numErrs + " errors and " + numWarnings 363 + " warnings."; 364 if (failureProperty != null) { 365 getProject().setProperty(failureProperty, failureMsg); 366 } 367 368 if (failOnViolation) { 369 throw new BuildException(failureMsg, getLocation()); 370 } 371 } 372 } 373 374 /** 375 * Creates new instance of the root module. 376 * @return new instance of the root module 377 */ 378 private RootModule createRootModule() { 379 final RootModule rootModule; 380 try { 381 final Properties props = createOverridingProperties(); 382 final ThreadModeSettings threadModeSettings = 383 ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE; 384 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions; 385 if (executeIgnoredModules) { 386 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE; 387 } 388 else { 389 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT; 390 } 391 392 final Configuration configuration = ConfigurationLoader.loadConfiguration(config, 393 new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings); 394 395 final ClassLoader moduleClassLoader = 396 Checker.class.getClassLoader(); 397 398 final ModuleFactory factory = new PackageObjectFactory( 399 Checker.class.getPackage().getName() + ".", moduleClassLoader); 400 401 rootModule = (RootModule) factory.createModule(configuration.getName()); 402 rootModule.setModuleClassLoader(moduleClassLoader); 403 rootModule.configure(configuration); 404 } 405 catch (final CheckstyleException ex) { 406 throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: " 407 + "config {%s}, classpath {%s}.", config, classpath), ex); 408 } 409 return rootModule; 410 } 411 412 /** 413 * Create the Properties object based on the arguments specified 414 * to the ANT task. 415 * @return the properties for property expansion expansion 416 */ 417 private Properties createOverridingProperties() { 418 final Properties returnValue = new Properties(); 419 420 // Load the properties file if specified 421 if (properties != null) { 422 try (InputStream inStream = Files.newInputStream(properties.toPath())) { 423 returnValue.load(inStream); 424 } 425 catch (final IOException ex) { 426 throw new BuildException("Error loading Properties file '" 427 + properties + "'", ex, getLocation()); 428 } 429 } 430 431 // override with Ant properties like ${basedir} 432 final Map<String, Object> antProps = getProject().getProperties(); 433 for (Map.Entry<String, Object> entry : antProps.entrySet()) { 434 final String value = String.valueOf(entry.getValue()); 435 returnValue.setProperty(entry.getKey(), value); 436 } 437 438 // override with properties specified in subelements 439 for (Property p : overrideProps) { 440 returnValue.setProperty(p.getKey(), p.getValue()); 441 } 442 443 return returnValue; 444 } 445 446 /** 447 * Return the list of listeners set in this task. 448 * @return the list of listeners. 449 */ 450 private AuditListener[] getListeners() { 451 final int formatterCount = Math.max(1, formatters.size()); 452 453 final AuditListener[] listeners = new AuditListener[formatterCount]; 454 455 // formatters 456 try { 457 if (formatters.isEmpty()) { 458 final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG); 459 final OutputStream err = new LogOutputStream(this, Project.MSG_ERR); 460 listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE, 461 err, AutomaticBean.OutputStreamOptions.CLOSE); 462 } 463 else { 464 for (int i = 0; i < formatterCount; i++) { 465 final Formatter formatter = formatters.get(i); 466 listeners[i] = formatter.createListener(this); 467 } 468 } 469 } 470 catch (IOException ex) { 471 throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: " 472 + "formatters {%s}.", formatters), ex); 473 } 474 return listeners; 475 } 476 477 /** 478 * Returns the list of files (full path name) to process. 479 * @return the list of files included via the fileName, filesets and paths. 480 */ 481 private List<File> getFilesToCheck() { 482 final List<File> allFiles = new ArrayList<>(); 483 if (fileName != null) { 484 // oops we've got an additional one to process, don't 485 // forget it. No sweat, it's fully resolved via the setter. 486 log("Adding standalone file for audit", Project.MSG_VERBOSE); 487 allFiles.add(new File(fileName)); 488 } 489 490 final List<File> filesFromFileSets = scanFileSets(); 491 allFiles.addAll(filesFromFileSets); 492 493 final List<File> filesFromPaths = scanPaths(); 494 allFiles.addAll(filesFromPaths); 495 496 return allFiles; 497 } 498 499 /** 500 * Retrieves all files from the defined paths. 501 * @return a list of files defined via paths. 502 */ 503 private List<File> scanPaths() { 504 final List<File> allFiles = new ArrayList<>(); 505 506 for (int i = 0; i < paths.size(); i++) { 507 final Path currentPath = paths.get(i); 508 final List<File> pathFiles = scanPath(currentPath, i + 1); 509 allFiles.addAll(pathFiles); 510 } 511 512 return allFiles; 513 } 514 515 /** 516 * Scans the given path and retrieves all files for the given path. 517 * 518 * @param path A path to scan. 519 * @param pathIndex The index of the given path. Used in log messages only. 520 * @return A list of files, extracted from the given path. 521 */ 522 private List<File> scanPath(Path path, int pathIndex) { 523 final String[] resources = path.list(); 524 log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE); 525 final List<File> allFiles = new ArrayList<>(); 526 int concreteFilesCount = 0; 527 528 for (String resource : resources) { 529 final File file = new File(resource); 530 if (file.isFile()) { 531 concreteFilesCount++; 532 allFiles.add(file); 533 } 534 else { 535 final DirectoryScanner scanner = new DirectoryScanner(); 536 scanner.setBasedir(file); 537 scanner.scan(); 538 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex); 539 allFiles.addAll(scannedFiles); 540 } 541 } 542 543 if (concreteFilesCount > 0) { 544 log(String.format(Locale.ROOT, "%d) Adding %d files from path %s", 545 pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE); 546 } 547 548 return allFiles; 549 } 550 551 /** 552 * Returns the list of files (full path name) to process. 553 * @return the list of files included via the filesets. 554 */ 555 protected List<File> scanFileSets() { 556 final List<File> allFiles = new ArrayList<>(); 557 558 for (int i = 0; i < fileSets.size(); i++) { 559 final FileSet fileSet = fileSets.get(i); 560 final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject()); 561 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i); 562 allFiles.addAll(scannedFiles); 563 } 564 565 return allFiles; 566 } 567 568 /** 569 * Retrieves all matched files from the given scanner. 570 * 571 * @param scanner A directory scanner. Note, that {@link DirectoryScanner#scan()} 572 * must be called before calling this method. 573 * @param logIndex A log entry index. Used only for log messages. 574 * @return A list of files, retrieved from the given scanner. 575 */ 576 private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) { 577 final String[] fileNames = scanner.getIncludedFiles(); 578 log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s", 579 logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE); 580 581 return Arrays.stream(fileNames) 582 .map(name -> scanner.getBasedir() + File.separator + name) 583 .map(File::new) 584 .collect(Collectors.toList()); 585 } 586 587 /** 588 * Poor mans enumeration for the formatter types. 589 */ 590 public static class FormatterType extends EnumeratedAttribute { 591 592 /** My possible values. */ 593 private static final String[] VALUES = {E_XML, E_PLAIN}; 594 595 @Override 596 public String[] getValues() { 597 return VALUES.clone(); 598 } 599 600 } 601 602 /** 603 * Details about a formatter to be used. 604 */ 605 public static class Formatter { 606 607 /** The formatter type. */ 608 private FormatterType type; 609 /** The file to output to. */ 610 private File toFile; 611 /** Whether or not the write to the named file. */ 612 private boolean useFile = true; 613 614 /** 615 * Set the type of the formatter. 616 * @param type the type 617 */ 618 public void setType(FormatterType type) { 619 this.type = type; 620 } 621 622 /** 623 * Set the file to output to. 624 * @param destination destination the file to output to 625 */ 626 public void setTofile(File destination) { 627 toFile = destination; 628 } 629 630 /** 631 * Sets whether or not we write to a file if it is provided. 632 * @param use whether not not to use provided file. 633 */ 634 public void setUseFile(boolean use) { 635 useFile = use; 636 } 637 638 /** 639 * Creates a listener for the formatter. 640 * @param task the task running 641 * @return a listener 642 * @throws IOException if an error occurs 643 */ 644 public AuditListener createListener(Task task) throws IOException { 645 final AuditListener listener; 646 if (type != null 647 && E_XML.equals(type.getValue())) { 648 listener = createXmlLogger(task); 649 } 650 else { 651 listener = createDefaultLogger(task); 652 } 653 return listener; 654 } 655 656 /** 657 * Creates default logger. 658 * @param task the task to possibly log to 659 * @return a DefaultLogger instance 660 * @throws IOException if an error occurs 661 */ 662 private AuditListener createDefaultLogger(Task task) 663 throws IOException { 664 final AuditListener defaultLogger; 665 if (toFile == null || !useFile) { 666 defaultLogger = new DefaultLogger( 667 new LogOutputStream(task, Project.MSG_DEBUG), 668 AutomaticBean.OutputStreamOptions.CLOSE, 669 new LogOutputStream(task, Project.MSG_ERR), 670 AutomaticBean.OutputStreamOptions.CLOSE 671 ); 672 } 673 else { 674 final OutputStream infoStream = Files.newOutputStream(toFile.toPath()); 675 defaultLogger = 676 new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE, 677 infoStream, AutomaticBean.OutputStreamOptions.NONE); 678 } 679 return defaultLogger; 680 } 681 682 /** 683 * Creates XML logger. 684 * @param task the task to possibly log to 685 * @return an XMLLogger instance 686 * @throws IOException if an error occurs 687 */ 688 private AuditListener createXmlLogger(Task task) throws IOException { 689 final AuditListener xmlLogger; 690 if (toFile == null || !useFile) { 691 xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO), 692 AutomaticBean.OutputStreamOptions.CLOSE); 693 } 694 else { 695 xmlLogger = new XMLLogger(Files.newOutputStream(toFile.toPath()), 696 AutomaticBean.OutputStreamOptions.CLOSE); 697 } 698 return xmlLogger; 699 } 700 701 } 702 703 /** 704 * Represents a property that consists of a key and value. 705 */ 706 public static class Property { 707 708 /** The property key. */ 709 private String key; 710 /** The property value. */ 711 private String value; 712 713 /** 714 * Gets key. 715 * @return the property key 716 */ 717 public String getKey() { 718 return key; 719 } 720 721 /** 722 * Sets key. 723 * @param key sets the property key 724 */ 725 public void setKey(String key) { 726 this.key = key; 727 } 728 729 /** 730 * Gets value. 731 * @return the property value 732 */ 733 public String getValue() { 734 return value; 735 } 736 737 /** 738 * Sets value. 739 * @param value set the property value 740 */ 741 public void setValue(String value) { 742 this.value = value; 743 } 744 745 /** 746 * Sets the property value from a File. 747 * @param file set the property value from a File 748 */ 749 public void setFile(File file) { 750 value = file.getAbsolutePath(); 751 } 752 753 } 754 755 /** Represents a custom listener. */ 756 public static class Listener { 757 758 /** Class name of the listener class. */ 759 private String className; 760 761 /** 762 * Gets class name. 763 * @return the class name 764 */ 765 public String getClassname() { 766 return className; 767 } 768 769 /** 770 * Sets class name. 771 * @param name set the class name 772 */ 773 public void setClassname(String name) { 774 className = name; 775 } 776 777 } 778 779}