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