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