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