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