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