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