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