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