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     */
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.  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    }