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