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