001    /*
002     * Copyright 2008-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.util.args;
022    
023    
024    
025    import java.io.BufferedReader;
026    import java.io.File;
027    import java.io.FileInputStream;
028    import java.io.FileReader;
029    import java.io.IOException;
030    import java.util.ArrayList;
031    import java.util.Collections;
032    import java.util.Iterator;
033    import java.util.List;
034    
035    import com.unboundid.util.Mutable;
036    import com.unboundid.util.ThreadSafety;
037    import com.unboundid.util.ThreadSafetyLevel;
038    
039    import static com.unboundid.util.args.ArgsMessages.*;
040    
041    
042    
043    /**
044     * This class defines an argument that is intended to hold values which refer to
045     * files on the local filesystem.  File arguments must take values, and it is
046     * possible to restrict the values to files that exist, or whose parent exists.
047     */
048    @Mutable()
049    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
050    public final class FileArgument
051           extends Argument
052    {
053      /**
054       * The serial version UID for this serializable class.
055       */
056      private static final long serialVersionUID = -8478637530068695898L;
057    
058    
059    
060      // Indicates whether values must represent files that exist.
061      private final boolean fileMustExist;
062    
063      // Indicates whether the provided value must be a directory if it exists.
064      private final boolean mustBeDirectory;
065    
066      // Indicates whether the provided value must be a regular file if it exists.
067      private final boolean mustBeFile;
068    
069      // Indicates whether values must represent files with parent directories that
070      // exist.
071      private final boolean parentMustExist;
072    
073      // The set of values assigned to this argument.
074      private final ArrayList<File> values;
075    
076      // The path to the directory that will serve as the base directory for
077      // relative paths.
078      private File relativeBaseDirectory;
079    
080      // The argument value validators that have been registered for this argument.
081      private final List<ArgumentValueValidator> validators;
082    
083      // The list of default values for this argument.
084      private final List<File> defaultValues;
085    
086    
087    
088      /**
089       * Creates a new file argument with the provided information.  It will not
090       * be required, will permit at most one occurrence, will use a default
091       * placeholder, will not have any default values, and will not impose any
092       * constraints on the kinds of values it can have.
093       *
094       * @param  shortIdentifier   The short identifier for this argument.  It may
095       *                           not be {@code null} if the long identifier is
096       *                           {@code null}.
097       * @param  longIdentifier    The long identifier for this argument.  It may
098       *                           not be {@code null} if the short identifier is
099       *                           {@code null}.
100       * @param  description       A human-readable description for this argument.
101       *                           It must not be {@code null}.
102       *
103       * @throws  ArgumentException  If there is a problem with the definition of
104       *                             this argument.
105       */
106      public FileArgument(final Character shortIdentifier,
107                          final String longIdentifier, final String description)
108             throws ArgumentException
109      {
110        this(shortIdentifier, longIdentifier, false, 1, null, description);
111      }
112    
113    
114    
115      /**
116       * Creates a new file argument with the provided information.  There will not
117       * be any default values or constraints on the kinds of values it can have.
118       *
119       * @param  shortIdentifier   The short identifier for this argument.  It may
120       *                           not be {@code null} if the long identifier is
121       *                           {@code null}.
122       * @param  longIdentifier    The long identifier for this argument.  It may
123       *                           not be {@code null} if the short identifier is
124       *                           {@code null}.
125       * @param  isRequired        Indicates whether this argument is required to
126       *                           be provided.
127       * @param  maxOccurrences    The maximum number of times this argument may be
128       *                           provided on the command line.  A value less than
129       *                           or equal to zero indicates that it may be present
130       *                           any number of times.
131       * @param  valuePlaceholder  A placeholder to display in usage information to
132       *                           indicate that a value must be provided.  It may
133       *                           be {@code null} if a default placeholder should
134       *                           be used.
135       * @param  description       A human-readable description for this argument.
136       *                           It must not be {@code null}.
137       *
138       * @throws  ArgumentException  If there is a problem with the definition of
139       *                             this argument.
140       */
141      public FileArgument(final Character shortIdentifier,
142                          final String longIdentifier, final boolean isRequired,
143                          final int maxOccurrences, final String valuePlaceholder,
144                          final String description)
145             throws ArgumentException
146      {
147        this(shortIdentifier, longIdentifier, isRequired,  maxOccurrences,
148             valuePlaceholder, description, false, false, false, false, null);
149      }
150    
151    
152    
153      /**
154       * Creates a new file argument with the provided information.  It will not
155       * have any default values.
156       *
157       * @param  shortIdentifier   The short identifier for this argument.  It may
158       *                           not be {@code null} if the long identifier is
159       *                           {@code null}.
160       * @param  longIdentifier    The long identifier for this argument.  It may
161       *                           not be {@code null} if the short identifier is
162       *                           {@code null}.
163       * @param  isRequired        Indicates whether this argument is required to
164       *                           be provided.
165       * @param  maxOccurrences    The maximum number of times this argument may be
166       *                           provided on the command line.  A value less than
167       *                           or equal to zero indicates that it may be present
168       *                           any number of times.
169       * @param  valuePlaceholder  A placeholder to display in usage information to
170       *                           indicate that a value must be provided.  It may
171       *                           be {@code null} if a default placeholder should
172       *                           be used.
173       * @param  description       A human-readable description for this argument.
174       *                           It must not be {@code null}.
175       * @param  fileMustExist     Indicates whether each value must refer to a file
176       *                           that exists.
177       * @param  parentMustExist   Indicates whether each value must refer to a file
178       *                           whose parent directory exists.
179       * @param  mustBeFile        Indicates whether each value must refer to a
180       *                           regular file, if it exists.
181       * @param  mustBeDirectory   Indicates whether each value must refer to a
182       *                           directory, if it exists.
183       *
184       * @throws  ArgumentException  If there is a problem with the definition of
185       *                             this argument.
186       */
187      public FileArgument(final Character shortIdentifier,
188                          final String longIdentifier, final boolean isRequired,
189                          final int maxOccurrences, final String valuePlaceholder,
190                          final String description, final boolean fileMustExist,
191                          final boolean parentMustExist, final boolean mustBeFile,
192                          final boolean mustBeDirectory)
193             throws ArgumentException
194      {
195        this(shortIdentifier, longIdentifier, isRequired, maxOccurrences,
196             valuePlaceholder, description, fileMustExist, parentMustExist,
197             mustBeFile, mustBeDirectory, null);
198      }
199    
200    
201    
202      /**
203       * Creates a new file argument with the provided information.
204       *
205       * @param  shortIdentifier   The short identifier for this argument.  It may
206       *                           not be {@code null} if the long identifier is
207       *                           {@code null}.
208       * @param  longIdentifier    The long identifier for this argument.  It may
209       *                           not be {@code null} if the short identifier is
210       *                           {@code null}.
211       * @param  isRequired        Indicates whether this argument is required to
212       *                           be provided.
213       * @param  maxOccurrences    The maximum number of times this argument may be
214       *                           provided on the command line.  A value less than
215       *                           or equal to zero indicates that it may be present
216       *                           any number of times.
217       * @param  valuePlaceholder  A placeholder to display in usage information to
218       *                           indicate that a value must be provided.  It may
219       *                           be {@code null} if a default placeholder should
220       *                           be used.
221       * @param  description       A human-readable description for this argument.
222       *                           It must not be {@code null}.
223       * @param  fileMustExist     Indicates whether each value must refer to a file
224       *                           that exists.
225       * @param  parentMustExist   Indicates whether each value must refer to a file
226       *                           whose parent directory exists.
227       * @param  mustBeFile        Indicates whether each value must refer to a
228       *                           regular file, if it exists.
229       * @param  mustBeDirectory   Indicates whether each value must refer to a
230       *                           directory, if it exists.
231       * @param  defaultValues     The set of default values to use for this
232       *                           argument if no values were provided.
233       *
234       * @throws  ArgumentException  If there is a problem with the definition of
235       *                             this argument.
236       */
237      public FileArgument(final Character shortIdentifier,
238                          final String longIdentifier, final boolean isRequired,
239                          final int maxOccurrences, final String valuePlaceholder,
240                          final String description, final boolean fileMustExist,
241                          final boolean parentMustExist, final boolean mustBeFile,
242                          final boolean mustBeDirectory,
243                          final List<File> defaultValues)
244             throws ArgumentException
245      {
246        super(shortIdentifier, longIdentifier, isRequired,  maxOccurrences,
247             (valuePlaceholder == null)
248                  ? INFO_PLACEHOLDER_PATH.get()
249                  : valuePlaceholder,
250             description);
251    
252        if (mustBeFile && mustBeDirectory)
253        {
254          throw new ArgumentException(ERR_FILE_CANNOT_BE_FILE_AND_DIRECTORY.get(
255                                           getIdentifierString()));
256        }
257    
258        this.fileMustExist   = fileMustExist;
259        this.parentMustExist = parentMustExist;
260        this.mustBeFile      = mustBeFile;
261        this.mustBeDirectory = mustBeDirectory;
262    
263        if ((defaultValues == null) || defaultValues.isEmpty())
264        {
265          this.defaultValues = null;
266        }
267        else
268        {
269          this.defaultValues = Collections.unmodifiableList(defaultValues);
270        }
271    
272        values                = new ArrayList<File>(5);
273        validators            = new ArrayList<ArgumentValueValidator>(5);
274        relativeBaseDirectory = null;
275      }
276    
277    
278    
279      /**
280       * Creates a new file argument that is a "clean" copy of the provided source
281       * argument.
282       *
283       * @param  source  The source argument to use for this argument.
284       */
285      private FileArgument(final FileArgument source)
286      {
287        super(source);
288    
289        fileMustExist         = source.fileMustExist;
290        mustBeDirectory       = source.mustBeDirectory;
291        mustBeFile            = source.mustBeFile;
292        parentMustExist       = source.parentMustExist;
293        defaultValues         = source.defaultValues;
294        relativeBaseDirectory = source.relativeBaseDirectory;
295        validators            =
296             new ArrayList<ArgumentValueValidator>(source.validators);
297        values                = new ArrayList<File>(5);
298      }
299    
300    
301    
302      /**
303       * Indicates whether each value must refer to a file that exists.
304       *
305       * @return  {@code true} if the target files must exist, or {@code false} if
306       *          it is acceptable for values to refer to files that do not exist.
307       */
308      public boolean fileMustExist()
309      {
310        return fileMustExist;
311      }
312    
313    
314    
315      /**
316       * Indicates whether each value must refer to a file whose parent directory
317       * exists.
318       *
319       * @return  {@code true} if the parent directory for target files must exist,
320       *          or {@code false} if it is acceptable for values to refer to files
321       *          whose parent directories do not exist.
322       */
323      public boolean parentMustExist()
324      {
325        return parentMustExist;
326      }
327    
328    
329    
330      /**
331       * Indicates whether each value must refer to a regular file (if it exists).
332       *
333       * @return  {@code true} if each value must refer to a regular file (if it
334       *          exists), or {@code false} if it may refer to a directory.
335       */
336      public boolean mustBeFile()
337      {
338        return mustBeFile;
339      }
340    
341    
342    
343      /**
344       * Indicates whether each value must refer to a directory (if it exists).
345       *
346       * @return  {@code true} if each value must refer to a directory (if it
347       *          exists), or {@code false} if it may refer to a regular file.
348       */
349      public boolean mustBeDirectory()
350      {
351        return mustBeDirectory;
352      }
353    
354    
355    
356      /**
357       * Retrieves the list of default values for this argument, which will be used
358       * if no values were provided.
359       *
360       * @return   The list of default values for this argument, or {@code null} if
361       *           there are no default values.
362       */
363      public List<File> getDefaultValues()
364      {
365        return defaultValues;
366      }
367    
368    
369    
370      /**
371       * Retrieves the directory that will serve as the base directory for relative
372       * paths, if one has been defined.
373       *
374       * @return  The directory that will serve as the base directory for relative
375       *          paths, or {@code null} if relative paths will be relative to the
376       *          current working directory.
377       */
378      public File getRelativeBaseDirectory()
379      {
380        return relativeBaseDirectory;
381      }
382    
383    
384    
385      /**
386       * Specifies the directory that will serve as the base directory for relative
387       * paths.
388       *
389       * @param  relativeBaseDirectory  The directory that will serve as the base
390       *                                directory for relative paths.  It may be
391       *                                {@code null} if relative paths should be
392       *                                relative to the current working directory.
393       */
394      public void setRelativeBaseDirectory(final File relativeBaseDirectory)
395      {
396        this.relativeBaseDirectory = relativeBaseDirectory;
397      }
398    
399    
400    
401      /**
402       * Updates this argument to ensure that the provided validator will be invoked
403       * for any values provided to this argument.  This validator will be invoked
404       * after all other validation has been performed for this argument.
405       *
406       * @param  validator  The argument value validator to be invoked.  It must not
407       *                    be {@code null}.
408       */
409      public void addValueValidator(final ArgumentValueValidator validator)
410      {
411        validators.add(validator);
412      }
413    
414    
415    
416      /**
417       * {@inheritDoc}
418       */
419      @Override()
420      protected void addValue(final String valueString)
421                throws ArgumentException
422      {
423        // NOTE:  java.io.File has an extremely weird behavior.  When a File object
424        // is created from a relative path and that path contains only the filename,
425        // then calling getParent or getParentFile will return null even though it
426        // obviously has a parent.  Therefore, you must always create a File using
427        // the absolute path if you might want to get the parent.  Also, if the path
428        // is relative, then we might want to control the base to which it is
429        // relative.
430        File f = new File(valueString);
431        if (! f.isAbsolute())
432        {
433          if (relativeBaseDirectory == null)
434          {
435            f = new File(f.getAbsolutePath());
436          }
437          else
438          {
439            f = new File(new File(relativeBaseDirectory,
440                 valueString).getAbsolutePath());
441          }
442        }
443    
444        if (f.exists())
445        {
446          if (mustBeFile && (! f.isFile()))
447          {
448            throw new ArgumentException(ERR_FILE_VALUE_NOT_FILE.get(
449                                             getIdentifierString(),
450                                             f.getAbsolutePath()));
451          }
452          else if (mustBeDirectory && (! f.isDirectory()))
453          {
454            throw new ArgumentException(ERR_FILE_VALUE_NOT_DIRECTORY.get(
455                                             getIdentifierString(),
456                                             f.getAbsolutePath()));
457          }
458        }
459        else
460        {
461          if (fileMustExist)
462          {
463            throw new ArgumentException(ERR_FILE_DOESNT_EXIST.get(
464                                             f.getAbsolutePath(),
465                                             getIdentifierString()));
466          }
467          else if (parentMustExist)
468          {
469            final File parentFile = f.getParentFile();
470            if ((parentFile == null) ||
471                (! parentFile.exists()) ||
472                (! parentFile.isDirectory()))
473            {
474              throw new ArgumentException(ERR_FILE_PARENT_DOESNT_EXIST.get(
475                                               f.getAbsolutePath(),
476                                               getIdentifierString()));
477            }
478          }
479        }
480    
481        if (values.size() >= getMaxOccurrences())
482        {
483          throw new ArgumentException(ERR_ARG_MAX_OCCURRENCES_EXCEEDED.get(
484                                           getIdentifierString()));
485        }
486    
487        for (final ArgumentValueValidator v : validators)
488        {
489          v.validateArgumentValue(this, valueString);
490        }
491    
492        values.add(f);
493      }
494    
495    
496    
497      /**
498       * Retrieves the value for this argument, or the default value if none was
499       * provided.  If there are multiple values, then the first will be returned.
500       *
501       * @return  The value for this argument, or the default value if none was
502       *          provided, or {@code null} if there is no value and no default
503       *          value.
504       */
505      public File getValue()
506      {
507        if (values.isEmpty())
508        {
509          if ((defaultValues == null) || defaultValues.isEmpty())
510          {
511            return null;
512          }
513          else
514          {
515            return defaultValues.get(0);
516          }
517        }
518        else
519        {
520          return values.get(0);
521        }
522      }
523    
524    
525    
526      /**
527       * Retrieves the set of values for this argument.
528       *
529       * @return  The set of values for this argument.
530       */
531      public List<File> getValues()
532      {
533        if (values.isEmpty() && (defaultValues != null))
534        {
535          return defaultValues;
536        }
537    
538        return Collections.unmodifiableList(values);
539      }
540    
541    
542    
543      /**
544       * Reads the contents of the file specified as the value to this argument and
545       * retrieves a list of the lines contained in it.  If there are multiple
546       * values for this argument, then the file specified as the first value will
547       * be used.
548       *
549       * @return  A list containing the lines of the target file, or {@code null} if
550       *          no values were provided.
551       *
552       * @throws  IOException  If the specified file does not exist or a problem
553       *                       occurs while reading the contents of the file.
554       */
555      public List<String> getFileLines()
556             throws IOException
557      {
558        final File f = getValue();
559        if (f == null)
560        {
561          return null;
562        }
563    
564        final ArrayList<String> lines  = new ArrayList<String>();
565        final BufferedReader    reader = new BufferedReader(new FileReader(f));
566        try
567        {
568          String line = reader.readLine();
569          while (line != null)
570          {
571            lines.add(line);
572            line = reader.readLine();
573          }
574        }
575        finally
576        {
577          reader.close();
578        }
579    
580        return lines;
581      }
582    
583    
584    
585      /**
586       * Reads the contents of the file specified as the value to this argument and
587       * retrieves a list of the non-blank lines contained in it.  If there are
588       * multiple values for this argument, then the file specified as the first
589       * value will be used.
590       *
591       * @return  A list containing the non-blank lines of the target file, or
592       *          {@code null} if no values were provided.
593       *
594       * @throws  IOException  If the specified file does not exist or a problem
595       *                       occurs while reading the contents of the file.
596       */
597      public List<String> getNonBlankFileLines()
598             throws IOException
599      {
600        final File f = getValue();
601        if (f == null)
602        {
603          return null;
604        }
605    
606        final ArrayList<String> lines = new ArrayList<String>();
607        final BufferedReader reader = new BufferedReader(new FileReader(f));
608        try
609        {
610          String line = reader.readLine();
611          while (line != null)
612          {
613            if (line.length() > 0)
614            {
615              lines.add(line);
616            }
617            line = reader.readLine();
618          }
619        }
620        finally
621        {
622          reader.close();
623        }
624    
625        return lines;
626      }
627    
628    
629    
630      /**
631       * Reads the contents of the file specified as the value to this argument.  If
632       * there are multiple values for this argument, then the file specified as the
633       * first value will be used.
634       *
635       * @return  A byte array containing the contents of the target file, or
636       *          {@code null} if no values were provided.
637       *
638       * @throws  IOException  If the specified file does not exist or a problem
639       *                       occurs while reading the contents of the file.
640       */
641      public byte[] getFileBytes()
642             throws IOException
643      {
644        final File f = getValue();
645        if (f == null)
646        {
647          return null;
648        }
649    
650        final byte[] fileData = new byte[(int) f.length()];
651        final FileInputStream inputStream = new FileInputStream(f);
652        try
653        {
654          int startPos  = 0;
655          int length    = fileData.length;
656          int bytesRead = inputStream.read(fileData, startPos, length);
657          while ((bytesRead > 0) && (startPos < fileData.length))
658          {
659            startPos += bytesRead;
660            length   -= bytesRead;
661            bytesRead = inputStream.read(fileData, startPos, length);
662          }
663    
664          if (startPos < fileData.length)
665          {
666            throw new IOException(ERR_FILE_CANNOT_READ_FULLY.get(
667                                       f.getAbsolutePath(), getIdentifierString()));
668          }
669    
670          return fileData;
671        }
672        finally
673        {
674          inputStream.close();
675        }
676      }
677    
678    
679    
680      /**
681       * {@inheritDoc}
682       */
683      @Override()
684      public List<String> getValueStringRepresentations(final boolean useDefault)
685      {
686        final List<File> files;
687        if (values.isEmpty())
688        {
689          if (useDefault)
690          {
691            files = defaultValues;
692          }
693          else
694          {
695            return Collections.emptyList();
696          }
697        }
698        else
699        {
700          files = values;
701        }
702    
703        if ((files == null) || files.isEmpty())
704        {
705          return Collections.emptyList();
706        }
707    
708        final ArrayList<String> valueStrings = new ArrayList<String>(files.size());
709        for (final File f : files)
710        {
711          valueStrings.add(f.getAbsolutePath());
712        }
713        return Collections.unmodifiableList(valueStrings);
714      }
715    
716    
717    
718      /**
719       * {@inheritDoc}
720       */
721      @Override()
722      protected boolean hasDefaultValue()
723      {
724        return ((defaultValues != null) && (! defaultValues.isEmpty()));
725      }
726    
727    
728    
729      /**
730       * {@inheritDoc}
731       */
732      @Override()
733      public String getDataTypeName()
734      {
735        if (mustBeDirectory)
736        {
737          return INFO_FILE_TYPE_PATH_DIRECTORY.get();
738        }
739        else
740        {
741          return INFO_FILE_TYPE_PATH_FILE.get();
742        }
743      }
744    
745    
746    
747      /**
748       * {@inheritDoc}
749       */
750      @Override()
751      public String getValueConstraints()
752      {
753        final StringBuilder buffer = new StringBuilder();
754    
755        if (mustBeDirectory)
756        {
757          if (fileMustExist)
758          {
759            buffer.append(INFO_FILE_CONSTRAINTS_DIR_MUST_EXIST.get());
760          }
761          else if (parentMustExist)
762          {
763            buffer.append(INFO_FILE_CONSTRAINTS_DIR_PARENT_MUST_EXIST.get());
764          }
765          else
766          {
767            buffer.append(INFO_FILE_CONSTRAINTS_DIR_MAY_EXIST.get());
768          }
769        }
770        else
771        {
772          if (fileMustExist)
773          {
774            buffer.append(INFO_FILE_CONSTRAINTS_FILE_MUST_EXIST.get());
775          }
776          else if (parentMustExist)
777          {
778            buffer.append(INFO_FILE_CONSTRAINTS_FILE_PARENT_MUST_EXIST.get());
779          }
780          else
781          {
782            buffer.append(INFO_FILE_CONSTRAINTS_FILE_MAY_EXIST.get());
783          }
784        }
785    
786        if (relativeBaseDirectory != null)
787        {
788          buffer.append("  ");
789          buffer.append(INFO_FILE_CONSTRAINTS_RELATIVE_PATH_SPECIFIED_ROOT.get(
790               relativeBaseDirectory.getAbsolutePath()));
791        }
792    
793        return buffer.toString();
794      }
795    
796    
797    
798      /**
799       * {@inheritDoc}
800       */
801      @Override()
802      protected void reset()
803      {
804        super.reset();
805        values.clear();
806      }
807    
808    
809    
810      /**
811       * {@inheritDoc}
812       */
813      @Override()
814      public FileArgument getCleanCopy()
815      {
816        return new FileArgument(this);
817      }
818    
819    
820    
821      /**
822       * {@inheritDoc}
823       */
824      @Override()
825      protected void addToCommandLine(final List<String> argStrings)
826      {
827        if (values != null)
828        {
829          for (final File f : values)
830          {
831            argStrings.add(getIdentifierString());
832            if (isSensitive())
833            {
834              argStrings.add("***REDACTED***");
835            }
836            else
837            {
838              argStrings.add(f.getAbsolutePath());
839            }
840          }
841        }
842      }
843    
844    
845    
846      /**
847       * {@inheritDoc}
848       */
849      @Override()
850      public void toString(final StringBuilder buffer)
851      {
852        buffer.append("FileArgument(");
853        appendBasicToStringInfo(buffer);
854    
855        buffer.append(", fileMustExist=");
856        buffer.append(fileMustExist);
857        buffer.append(", parentMustExist=");
858        buffer.append(parentMustExist);
859        buffer.append(", mustBeFile=");
860        buffer.append(mustBeFile);
861        buffer.append(", mustBeDirectory=");
862        buffer.append(mustBeDirectory);
863    
864        if (relativeBaseDirectory != null)
865        {
866          buffer.append(", relativeBaseDirectory='");
867          buffer.append(relativeBaseDirectory.getAbsolutePath());
868          buffer.append('\'');
869        }
870    
871        if ((defaultValues != null) && (! defaultValues.isEmpty()))
872        {
873          if (defaultValues.size() == 1)
874          {
875            buffer.append(", defaultValue='");
876            buffer.append(defaultValues.get(0).toString());
877          }
878          else
879          {
880            buffer.append(", defaultValues={");
881    
882            final Iterator<File> iterator = defaultValues.iterator();
883            while (iterator.hasNext())
884            {
885              buffer.append('\'');
886              buffer.append(iterator.next().toString());
887              buffer.append('\'');
888    
889              if (iterator.hasNext())
890              {
891                buffer.append(", ");
892              }
893            }
894    
895            buffer.append('}');
896          }
897        }
898    
899        buffer.append(')');
900      }
901    }