001    /*
002     * Copyright 2008-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2013 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 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    }