001/*
002 * Copyright 2008-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 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 */
021package com.unboundid.util.args;
022
023
024
025import java.io.BufferedReader;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileReader;
029import java.io.IOException;
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.Iterator;
033import java.util.List;
034
035import com.unboundid.util.Mutable;
036import com.unboundid.util.ThreadSafety;
037import com.unboundid.util.ThreadSafetyLevel;
038
039import 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)
050public 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 list of default values for this argument.
081  private final List<File> defaultValues;
082
083
084
085  /**
086   * Creates a new file argument with the provided information.  There will not
087   * be any default values or constraints on the kinds of values it can have.
088   *
089   * @param  shortIdentifier   The short identifier for this argument.  It may
090   *                           not be {@code null} if the long identifier is
091   *                           {@code null}.
092   * @param  longIdentifier    The long identifier for this argument.  It may
093   *                           not be {@code null} if the short identifier is
094   *                           {@code null}.
095   * @param  isRequired        Indicates whether this argument is required to
096   *                           be provided.
097   * @param  maxOccurrences    The maximum number of times this argument may be
098   *                           provided on the command line.  A value less than
099   *                           or equal to zero indicates that it may be present
100   *                           any number of times.
101   * @param  valuePlaceholder  A placeholder to display in usage information to
102   *                           indicate that a value must be provided.  It must
103   *                           not be {@code null}.
104   * @param  description       A human-readable description for this argument.
105   *                           It must not be {@code null}.
106   *
107   * @throws  ArgumentException  If there is a problem with the definition of
108   *                             this argument.
109   */
110  public FileArgument(final Character shortIdentifier,
111                      final String longIdentifier, final boolean isRequired,
112                      final int maxOccurrences, final String valuePlaceholder,
113                      final String description)
114         throws ArgumentException
115  {
116    this(shortIdentifier, longIdentifier, isRequired,  maxOccurrences,
117         valuePlaceholder, description, false, false, false, false, null);
118  }
119
120
121
122  /**
123   * Creates a new file argument with the provided information.  It will not
124   * have any default values.
125   *
126   * @param  shortIdentifier   The short identifier for this argument.  It may
127   *                           not be {@code null} if the long identifier is
128   *                           {@code null}.
129   * @param  longIdentifier    The long identifier for this argument.  It may
130   *                           not be {@code null} if the short identifier is
131   *                           {@code null}.
132   * @param  isRequired        Indicates whether this argument is required to
133   *                           be provided.
134   * @param  maxOccurrences    The maximum number of times this argument may be
135   *                           provided on the command line.  A value less than
136   *                           or equal to zero indicates that it may be present
137   *                           any number of times.
138   * @param  valuePlaceholder  A placeholder to display in usage information to
139   *                           indicate that a value must be provided.  It must
140   *                           not be {@code null}.
141   * @param  description       A human-readable description for this argument.
142   *                           It must not be {@code null}.
143   * @param  fileMustExist     Indicates whether each value must refer to a file
144   *                           that exists.
145   * @param  parentMustExist   Indicates whether each value must refer to a file
146   *                           whose parent directory exists.
147   * @param  mustBeFile        Indicates whether each value must refer to a
148   *                           regular file, if it exists.
149   * @param  mustBeDirectory   Indicates whether each value must refer to a
150   *                           directory, if it exists.
151   *
152   * @throws  ArgumentException  If there is a problem with the definition of
153   *                             this argument.
154   */
155  public FileArgument(final Character shortIdentifier,
156                      final String longIdentifier, final boolean isRequired,
157                      final int maxOccurrences, final String valuePlaceholder,
158                      final String description, final boolean fileMustExist,
159                      final boolean parentMustExist, final boolean mustBeFile,
160                      final boolean mustBeDirectory)
161         throws ArgumentException
162  {
163    this(shortIdentifier, longIdentifier, isRequired, maxOccurrences,
164         valuePlaceholder, description, fileMustExist, parentMustExist,
165         mustBeFile, mustBeDirectory, null);
166  }
167
168
169
170  /**
171   * Creates a new file argument with the provided information.
172   *
173   * @param  shortIdentifier   The short identifier for this argument.  It may
174   *                           not be {@code null} if the long identifier is
175   *                           {@code null}.
176   * @param  longIdentifier    The long identifier for this argument.  It may
177   *                           not be {@code null} if the short identifier is
178   *                           {@code null}.
179   * @param  isRequired        Indicates whether this argument is required to
180   *                           be provided.
181   * @param  maxOccurrences    The maximum number of times this argument may be
182   *                           provided on the command line.  A value less than
183   *                           or equal to zero indicates that it may be present
184   *                           any number of times.
185   * @param  valuePlaceholder  A placeholder to display in usage information to
186   *                           indicate that a value must be provided.  It must
187   *                           not be {@code null}.
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   * @param  defaultValues     The set of default values to use for this
199   *                           argument if no values were provided.
200   *
201   * @throws  ArgumentException  If there is a problem with the definition of
202   *                             this argument.
203   */
204  public FileArgument(final Character shortIdentifier,
205                      final String longIdentifier, final boolean isRequired,
206                      final int maxOccurrences, final String valuePlaceholder,
207                      final String description, final boolean fileMustExist,
208                      final boolean parentMustExist, final boolean mustBeFile,
209                      final boolean mustBeDirectory,
210                      final List<File> defaultValues)
211         throws ArgumentException
212  {
213    super(shortIdentifier, longIdentifier, isRequired,  maxOccurrences,
214          valuePlaceholder, description);
215
216    if (valuePlaceholder == null)
217    {
218      throw new ArgumentException(ERR_ARG_MUST_TAKE_VALUE.get(
219                                       getIdentifierString()));
220    }
221
222    if (mustBeFile && mustBeDirectory)
223    {
224      throw new ArgumentException(ERR_FILE_CANNOT_BE_FILE_AND_DIRECTORY.get(
225                                       getIdentifierString()));
226    }
227
228    this.fileMustExist   = fileMustExist;
229    this.parentMustExist = parentMustExist;
230    this.mustBeFile      = mustBeFile;
231    this.mustBeDirectory = mustBeDirectory;
232
233    if ((defaultValues == null) || defaultValues.isEmpty())
234    {
235      this.defaultValues = null;
236    }
237    else
238    {
239      this.defaultValues = Collections.unmodifiableList(defaultValues);
240    }
241
242    values                = new ArrayList<File>();
243    relativeBaseDirectory = null;
244  }
245
246
247
248  /**
249   * Creates a new file argument that is a "clean" copy of the provided source
250   * argument.
251   *
252   * @param  source  The source argument to use for this argument.
253   */
254  private FileArgument(final FileArgument source)
255  {
256    super(source);
257
258    fileMustExist         = source.fileMustExist;
259    mustBeDirectory       = source.mustBeDirectory;
260    mustBeFile            = source.mustBeFile;
261    parentMustExist       = source.parentMustExist;
262    defaultValues         = source.defaultValues;
263    relativeBaseDirectory = source.relativeBaseDirectory;
264         values           = new ArrayList<File>();
265  }
266
267
268
269  /**
270   * Indicates whether each value must refer to a file that exists.
271   *
272   * @return  {@code true} if the target files must exist, or {@code false} if
273   *          it is acceptable for values to refer to files that do not exist.
274   */
275  public boolean fileMustExist()
276  {
277    return fileMustExist;
278  }
279
280
281
282  /**
283   * Indicates whether each value must refer to a file whose parent directory
284   * exists.
285   *
286   * @return  {@code true} if the parent directory for target files must exist,
287   *          or {@code false} if it is acceptable for values to refer to files
288   *          whose parent directories do not exist.
289   */
290  public boolean parentMustExist()
291  {
292    return parentMustExist;
293  }
294
295
296
297  /**
298   * Indicates whether each value must refer to a regular file (if it exists).
299   *
300   * @return  {@code true} if each value must refer to a regular file (if it
301   *          exists), or {@code false} if it may refer to a directory.
302   */
303  public boolean mustBeFile()
304  {
305    return mustBeFile;
306  }
307
308
309
310  /**
311   * Indicates whether each value must refer to a directory (if it exists).
312   *
313   * @return  {@code true} if each value must refer to a directory (if it
314   *          exists), or {@code false} if it may refer to a regular file.
315   */
316  public boolean mustBeDirectory()
317  {
318    return mustBeDirectory;
319  }
320
321
322
323  /**
324   * Retrieves the list of default values for this argument, which will be used
325   * if no values were provided.
326   *
327   * @return   The list of default values for this argument, or {@code null} if
328   *           there are no default values.
329   */
330  public List<File> getDefaultValues()
331  {
332    return defaultValues;
333  }
334
335
336
337  /**
338   * Retrieves the directory that will serve as the base directory for relative
339   * paths, if one has been defined.
340   *
341   * @return  The directory that will serve as the base directory for relative
342   *          paths, or {@code null} if relative paths will be relative to the
343   *          current working directory.
344   */
345  public File getRelativeBaseDirectory()
346  {
347    return relativeBaseDirectory;
348  }
349
350
351
352  /**
353   * Specifies the directory that will serve as the base directory for relative
354   * paths.
355   *
356   * @param  relativeBaseDirectory  The directory that will serve as the base
357   *                                directory for relative paths.  It may be
358   *                                {@code null} if relative paths should be
359   *                                relative to the current working directory.
360   */
361  public void setRelativeBaseDirectory(final File relativeBaseDirectory)
362  {
363    this.relativeBaseDirectory = relativeBaseDirectory;
364  }
365
366
367
368  /**
369   * {@inheritDoc}
370   */
371  @Override()
372  protected void addValue(final String valueString)
373            throws ArgumentException
374  {
375    // NOTE:  java.io.File has an extremely weird behavior.  When a File object
376    // is created from a relative path and that path contains only the filename,
377    // then calling getParent or getParentFile will return null even though it
378    // obviously has a parent.  Therefore, you must always create a File using
379    // the absolute path if you might want to get the parent.  Also, if the path
380    // is relative, then we might want to control the base to which it is
381    // relative.
382    File f = new File(valueString);
383    if (! f.isAbsolute())
384    {
385      if (relativeBaseDirectory == null)
386      {
387        f = new File(f.getAbsolutePath());
388      }
389      else
390      {
391        f = new File(new File(relativeBaseDirectory,
392             valueString).getAbsolutePath());
393      }
394    }
395
396    if (f.exists())
397    {
398      if (mustBeFile && (! f.isFile()))
399      {
400        throw new ArgumentException(ERR_FILE_VALUE_NOT_FILE.get(
401                                         getIdentifierString(),
402                                         f.getAbsolutePath()));
403      }
404      else if (mustBeDirectory && (! f.isDirectory()))
405      {
406        throw new ArgumentException(ERR_FILE_VALUE_NOT_DIRECTORY.get(
407                                         getIdentifierString(),
408                                         f.getAbsolutePath()));
409      }
410    }
411    else
412    {
413      if (fileMustExist)
414      {
415        throw new ArgumentException(ERR_FILE_DOESNT_EXIST.get(
416                                         f.getAbsolutePath(),
417                                         getIdentifierString()));
418      }
419      else if (parentMustExist)
420      {
421        final File parentFile = f.getParentFile();
422        if ((parentFile == null) ||
423            (! parentFile.exists()) ||
424            (! parentFile.isDirectory()))
425        {
426          throw new ArgumentException(ERR_FILE_PARENT_DOESNT_EXIST.get(
427                                           f.getAbsolutePath(),
428                                           getIdentifierString()));
429        }
430      }
431    }
432
433    if (values.size() >= getMaxOccurrences())
434    {
435      throw new ArgumentException(ERR_ARG_MAX_OCCURRENCES_EXCEEDED.get(
436                                       getIdentifierString()));
437    }
438
439    values.add(f);
440  }
441
442
443
444  /**
445   * Retrieves the value for this argument, or the default value if none was
446   * provided.  If there are multiple values, then the first will be returned.
447   *
448   * @return  The value for this argument, or the default value if none was
449   *          provided, or {@code null} if there is no value and no default
450   *          value.
451   */
452  public File getValue()
453  {
454    if (values.isEmpty())
455    {
456      if ((defaultValues == null) || defaultValues.isEmpty())
457      {
458        return null;
459      }
460      else
461      {
462        return defaultValues.get(0);
463      }
464    }
465    else
466    {
467      return values.get(0);
468    }
469  }
470
471
472
473  /**
474   * Retrieves the set of values for this argument.
475   *
476   * @return  The set of values for this argument.
477   */
478  public List<File> getValues()
479  {
480    if (values.isEmpty() && (defaultValues != null))
481    {
482      return defaultValues;
483    }
484
485    return Collections.unmodifiableList(values);
486  }
487
488
489
490  /**
491   * Reads the contents of the file specified as the value to this argument and
492   * retrieves a list of the lines contained in it.  If there are multiple
493   * values for this argument, then the file specified as the first value will
494   * be used.
495   *
496   * @return  A list containing the lines of the target file, or {@code null} if
497   *          no values were provided.
498   *
499   * @throws  IOException  If the specified file does not exist or a problem
500   *                       occurs while reading the contents of the file.
501   */
502  public List<String> getFileLines()
503         throws IOException
504  {
505    final File f = getValue();
506    if (f == null)
507    {
508      return null;
509    }
510
511    final ArrayList<String> lines  = new ArrayList<String>();
512    final BufferedReader    reader = new BufferedReader(new FileReader(f));
513    try
514    {
515      String line = reader.readLine();
516      while (line != null)
517      {
518        lines.add(line);
519        line = reader.readLine();
520      }
521    }
522    finally
523    {
524      reader.close();
525    }
526
527    return lines;
528  }
529
530
531
532  /**
533   * Reads the contents of the file specified as the value to this argument and
534   * retrieves a list of the non-blank lines contained in it.  If there are
535   * multiple values for this argument, then the file specified as the first
536   * value will be used.
537   *
538   * @return  A list containing the non-blank lines of the target file, or
539   *          {@code null} if no values were provided.
540   *
541   * @throws  IOException  If the specified file does not exist or a problem
542   *                       occurs while reading the contents of the file.
543   */
544  public List<String> getNonBlankFileLines()
545         throws IOException
546  {
547    final File f = getValue();
548    if (f == null)
549    {
550      return null;
551    }
552
553    final ArrayList<String> lines = new ArrayList<String>();
554    final BufferedReader reader = new BufferedReader(new FileReader(f));
555    try
556    {
557      String line = reader.readLine();
558      while (line != null)
559      {
560        if (line.length() > 0)
561        {
562          lines.add(line);
563        }
564        line = reader.readLine();
565      }
566    }
567    finally
568    {
569      reader.close();
570    }
571
572    return lines;
573  }
574
575
576
577  /**
578   * Reads the contents of the file specified as the value to this argument.  If
579   * there are multiple values for this argument, then the file specified as the
580   * first value will be used.
581   *
582   * @return  A byte array containing the contents of the target file, or
583   *          {@code null} if no values were provided.
584   *
585   * @throws  IOException  If the specified file does not exist or a problem
586   *                       occurs while reading the contents of the file.
587   */
588  public byte[] getFileBytes()
589         throws IOException
590  {
591    final File f = getValue();
592    if (f == null)
593    {
594      return null;
595    }
596
597    final byte[] fileData = new byte[(int) f.length()];
598    final FileInputStream inputStream = new FileInputStream(f);
599    try
600    {
601      int startPos  = 0;
602      int length    = fileData.length;
603      int bytesRead = inputStream.read(fileData, startPos, length);
604      while ((bytesRead > 0) && (startPos < fileData.length))
605      {
606        startPos += bytesRead;
607        length   -= bytesRead;
608        bytesRead = inputStream.read(fileData, startPos, length);
609      }
610
611      if (startPos < fileData.length)
612      {
613        throw new IOException(ERR_FILE_CANNOT_READ_FULLY.get(
614                                   f.getAbsolutePath(), getIdentifierString()));
615      }
616
617      return fileData;
618    }
619    finally
620    {
621      inputStream.close();
622    }
623  }
624
625
626
627  /**
628   * {@inheritDoc}
629   */
630  @Override()
631  protected boolean hasDefaultValue()
632  {
633    return ((defaultValues != null) && (! defaultValues.isEmpty()));
634  }
635
636
637
638  /**
639   * {@inheritDoc}
640   */
641  @Override()
642  public String getDataTypeName()
643  {
644    if (mustBeDirectory)
645    {
646      return INFO_FILE_TYPE_PATH_DIRECTORY.get();
647    }
648    else
649    {
650      return INFO_FILE_TYPE_PATH_FILE.get();
651    }
652  }
653
654
655
656  /**
657   * {@inheritDoc}
658   */
659  @Override()
660  public String getValueConstraints()
661  {
662    final StringBuilder buffer = new StringBuilder();
663
664    if (mustBeDirectory)
665    {
666      if (fileMustExist)
667      {
668        buffer.append(INFO_FILE_CONSTRAINTS_DIR_MUST_EXIST.get());
669      }
670      else if (parentMustExist)
671      {
672        buffer.append(INFO_FILE_CONSTRAINTS_DIR_PARENT_MUST_EXIST.get());
673      }
674      else
675      {
676        buffer.append(INFO_FILE_CONSTRAINTS_DIR_MAY_EXIST.get());
677      }
678    }
679    else
680    {
681      if (fileMustExist)
682      {
683        buffer.append(INFO_FILE_CONSTRAINTS_FILE_MUST_EXIST.get());
684      }
685      else if (parentMustExist)
686      {
687        buffer.append(INFO_FILE_CONSTRAINTS_FILE_PARENT_MUST_EXIST.get());
688      }
689      else
690      {
691        buffer.append(INFO_FILE_CONSTRAINTS_FILE_MAY_EXIST.get());
692      }
693    }
694
695    if (relativeBaseDirectory != null)
696    {
697      buffer.append("  ");
698      buffer.append(INFO_FILE_CONSTRAINTS_RELATIVE_PATH_SPECIFIED_ROOT.get(
699           relativeBaseDirectory.getAbsolutePath()));
700    }
701
702    return buffer.toString();
703  }
704
705
706
707  /**
708   * {@inheritDoc}
709   */
710  @Override()
711  public FileArgument getCleanCopy()
712  {
713    return new FileArgument(this);
714  }
715
716
717
718  /**
719   * {@inheritDoc}
720   */
721  @Override()
722  public void toString(final StringBuilder buffer)
723  {
724    buffer.append("FileArgument(");
725    appendBasicToStringInfo(buffer);
726
727    buffer.append(", fileMustExist=");
728    buffer.append(fileMustExist);
729    buffer.append(", parentMustExist=");
730    buffer.append(parentMustExist);
731    buffer.append(", mustBeFile=");
732    buffer.append(mustBeFile);
733    buffer.append(", mustBeDirectory=");
734    buffer.append(mustBeDirectory);
735
736    if (relativeBaseDirectory != null)
737    {
738      buffer.append(", relativeBaseDirectory='");
739      buffer.append(relativeBaseDirectory.getAbsolutePath());
740      buffer.append('\'');
741    }
742
743    if ((defaultValues != null) && (! defaultValues.isEmpty()))
744    {
745      if (defaultValues.size() == 1)
746      {
747        buffer.append(", defaultValue='");
748        buffer.append(defaultValues.get(0).toString());
749      }
750      else
751      {
752        buffer.append(", defaultValues={");
753
754        final Iterator<File> iterator = defaultValues.iterator();
755        while (iterator.hasNext())
756        {
757          buffer.append('\'');
758          buffer.append(iterator.next().toString());
759          buffer.append('\'');
760
761          if (iterator.hasNext())
762          {
763            buffer.append(", ");
764          }
765        }
766
767        buffer.append('}');
768      }
769    }
770
771    buffer.append(')');
772  }
773}