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