001/* 002 * Copyright 2008-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2019 Ping Identity Corporation 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<>(5); 273 validators = new ArrayList<>(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 = new ArrayList<>(source.validators); 296 values = new ArrayList<>(5); 297 } 298 299 300 301 /** 302 * Indicates whether each value must refer to a file that exists. 303 * 304 * @return {@code true} if the target files must exist, or {@code false} if 305 * it is acceptable for values to refer to files that do not exist. 306 */ 307 public boolean fileMustExist() 308 { 309 return fileMustExist; 310 } 311 312 313 314 /** 315 * Indicates whether each value must refer to a file whose parent directory 316 * exists. 317 * 318 * @return {@code true} if the parent directory for target files must exist, 319 * or {@code false} if it is acceptable for values to refer to files 320 * whose parent directories do not exist. 321 */ 322 public boolean parentMustExist() 323 { 324 return parentMustExist; 325 } 326 327 328 329 /** 330 * Indicates whether each value must refer to a regular file (if it exists). 331 * 332 * @return {@code true} if each value must refer to a regular file (if it 333 * exists), or {@code false} if it may refer to a directory. 334 */ 335 public boolean mustBeFile() 336 { 337 return mustBeFile; 338 } 339 340 341 342 /** 343 * Indicates whether each value must refer to a directory (if it exists). 344 * 345 * @return {@code true} if each value must refer to a directory (if it 346 * exists), or {@code false} if it may refer to a regular file. 347 */ 348 public boolean mustBeDirectory() 349 { 350 return mustBeDirectory; 351 } 352 353 354 355 /** 356 * Retrieves the list of default values for this argument, which will be used 357 * if no values were provided. 358 * 359 * @return The list of default values for this argument, or {@code null} if 360 * there are no default values. 361 */ 362 public List<File> getDefaultValues() 363 { 364 return defaultValues; 365 } 366 367 368 369 /** 370 * Retrieves the directory that will serve as the base directory for relative 371 * paths, if one has been defined. 372 * 373 * @return The directory that will serve as the base directory for relative 374 * paths, or {@code null} if relative paths will be relative to the 375 * current working directory. 376 */ 377 public File getRelativeBaseDirectory() 378 { 379 return relativeBaseDirectory; 380 } 381 382 383 384 /** 385 * Specifies the directory that will serve as the base directory for relative 386 * paths. 387 * 388 * @param relativeBaseDirectory The directory that will serve as the base 389 * directory for relative paths. It may be 390 * {@code null} if relative paths should be 391 * relative to the current working directory. 392 */ 393 public void setRelativeBaseDirectory(final File relativeBaseDirectory) 394 { 395 this.relativeBaseDirectory = relativeBaseDirectory; 396 } 397 398 399 400 /** 401 * Updates this argument to ensure that the provided validator will be invoked 402 * for any values provided to this argument. This validator will be invoked 403 * after all other validation has been performed for this argument. 404 * 405 * @param validator The argument value validator to be invoked. It must not 406 * be {@code null}. 407 */ 408 public void addValueValidator(final ArgumentValueValidator validator) 409 { 410 validators.add(validator); 411 } 412 413 414 415 /** 416 * {@inheritDoc} 417 */ 418 @Override() 419 protected void addValue(final String valueString) 420 throws ArgumentException 421 { 422 // NOTE: java.io.File has an extremely weird behavior. When a File object 423 // is created from a relative path and that path contains only the filename, 424 // then calling getParent or getParentFile will return null even though it 425 // obviously has a parent. Therefore, you must always create a File using 426 // the absolute path if you might want to get the parent. Also, if the path 427 // is relative, then we might want to control the base to which it is 428 // relative. 429 File f = new File(valueString); 430 if (! f.isAbsolute()) 431 { 432 if (relativeBaseDirectory == null) 433 { 434 f = new File(f.getAbsolutePath()); 435 } 436 else 437 { 438 f = new File(new File(relativeBaseDirectory, 439 valueString).getAbsolutePath()); 440 } 441 } 442 443 if (f.exists()) 444 { 445 if (mustBeFile && (! f.isFile())) 446 { 447 throw new ArgumentException(ERR_FILE_VALUE_NOT_FILE.get( 448 getIdentifierString(), 449 f.getAbsolutePath())); 450 } 451 else if (mustBeDirectory && (! f.isDirectory())) 452 { 453 throw new ArgumentException(ERR_FILE_VALUE_NOT_DIRECTORY.get( 454 getIdentifierString(), 455 f.getAbsolutePath())); 456 } 457 } 458 else 459 { 460 if (fileMustExist) 461 { 462 throw new ArgumentException(ERR_FILE_DOESNT_EXIST.get( 463 f.getAbsolutePath(), 464 getIdentifierString())); 465 } 466 else if (parentMustExist) 467 { 468 final File parentFile = f.getAbsoluteFile().getParentFile(); 469 if ((parentFile == null) || 470 (! parentFile.exists()) || 471 (! parentFile.isDirectory())) 472 { 473 throw new ArgumentException(ERR_FILE_PARENT_DOESNT_EXIST.get( 474 f.getAbsolutePath(), 475 getIdentifierString())); 476 } 477 } 478 } 479 480 if (values.size() >= getMaxOccurrences()) 481 { 482 throw new ArgumentException(ERR_ARG_MAX_OCCURRENCES_EXCEEDED.get( 483 getIdentifierString())); 484 } 485 486 for (final ArgumentValueValidator v : validators) 487 { 488 v.validateArgumentValue(this, valueString); 489 } 490 491 values.add(f); 492 } 493 494 495 496 /** 497 * Retrieves the value for this argument, or the default value if none was 498 * provided. If there are multiple values, then the first will be returned. 499 * 500 * @return The value for this argument, or the default value if none was 501 * provided, or {@code null} if there is no value and no default 502 * value. 503 */ 504 public File getValue() 505 { 506 if (values.isEmpty()) 507 { 508 if ((defaultValues == null) || defaultValues.isEmpty()) 509 { 510 return null; 511 } 512 else 513 { 514 return defaultValues.get(0); 515 } 516 } 517 else 518 { 519 return values.get(0); 520 } 521 } 522 523 524 525 /** 526 * Retrieves the set of values for this argument. 527 * 528 * @return The set of values for this argument. 529 */ 530 public List<File> getValues() 531 { 532 if (values.isEmpty() && (defaultValues != null)) 533 { 534 return defaultValues; 535 } 536 537 return Collections.unmodifiableList(values); 538 } 539 540 541 542 /** 543 * Reads the contents of the file specified as the value to this argument and 544 * retrieves a list of the lines contained in it. If there are multiple 545 * values for this argument, then the file specified as the first value will 546 * be used. 547 * 548 * @return A list containing the lines of the target file, or {@code null} if 549 * no values were provided. 550 * 551 * @throws IOException If the specified file does not exist or a problem 552 * occurs while reading the contents of the file. 553 */ 554 public List<String> getFileLines() 555 throws IOException 556 { 557 final File f = getValue(); 558 if (f == null) 559 { 560 return null; 561 } 562 563 final ArrayList<String> lines = new ArrayList<>(20); 564 final BufferedReader reader = new BufferedReader(new FileReader(f)); 565 try 566 { 567 String line = reader.readLine(); 568 while (line != null) 569 { 570 lines.add(line); 571 line = reader.readLine(); 572 } 573 } 574 finally 575 { 576 reader.close(); 577 } 578 579 return lines; 580 } 581 582 583 584 /** 585 * Reads the contents of the file specified as the value to this argument and 586 * retrieves a list of the non-blank lines contained in it. If there are 587 * multiple values for this argument, then the file specified as the first 588 * value will be used. 589 * 590 * @return A list containing the non-blank lines of the target file, or 591 * {@code null} if no values were provided. 592 * 593 * @throws IOException If the specified file does not exist or a problem 594 * occurs while reading the contents of the file. 595 */ 596 public List<String> getNonBlankFileLines() 597 throws IOException 598 { 599 final File f = getValue(); 600 if (f == null) 601 { 602 return null; 603 } 604 605 final ArrayList<String> lines = new ArrayList<>(20); 606 final BufferedReader reader = new BufferedReader(new FileReader(f)); 607 try 608 { 609 String line = reader.readLine(); 610 while (line != null) 611 { 612 if (! line.isEmpty()) 613 { 614 lines.add(line); 615 } 616 line = reader.readLine(); 617 } 618 } 619 finally 620 { 621 reader.close(); 622 } 623 624 return lines; 625 } 626 627 628 629 /** 630 * Reads the contents of the file specified as the value to this argument. If 631 * there are multiple values for this argument, then the file specified as the 632 * first value will be used. 633 * 634 * @return A byte array containing the contents of the target file, or 635 * {@code null} if no values were provided. 636 * 637 * @throws IOException If the specified file does not exist or a problem 638 * occurs while reading the contents of the file. 639 */ 640 public byte[] getFileBytes() 641 throws IOException 642 { 643 final File f = getValue(); 644 if (f == null) 645 { 646 return null; 647 } 648 649 final byte[] fileData = new byte[(int) f.length()]; 650 final FileInputStream inputStream = new FileInputStream(f); 651 try 652 { 653 int startPos = 0; 654 int length = fileData.length; 655 int bytesRead = inputStream.read(fileData, startPos, length); 656 while ((bytesRead > 0) && (startPos < fileData.length)) 657 { 658 startPos += bytesRead; 659 length -= bytesRead; 660 bytesRead = inputStream.read(fileData, startPos, length); 661 } 662 663 if (startPos < fileData.length) 664 { 665 throw new IOException(ERR_FILE_CANNOT_READ_FULLY.get( 666 f.getAbsolutePath(), getIdentifierString())); 667 } 668 669 return fileData; 670 } 671 finally 672 { 673 inputStream.close(); 674 } 675 } 676 677 678 679 /** 680 * {@inheritDoc} 681 */ 682 @Override() 683 public List<String> getValueStringRepresentations(final boolean useDefault) 684 { 685 final List<File> files; 686 if (values.isEmpty()) 687 { 688 if (useDefault) 689 { 690 files = defaultValues; 691 } 692 else 693 { 694 return Collections.emptyList(); 695 } 696 } 697 else 698 { 699 files = values; 700 } 701 702 if ((files == null) || files.isEmpty()) 703 { 704 return Collections.emptyList(); 705 } 706 707 final ArrayList<String> valueStrings = new ArrayList<>(files.size()); 708 for (final File f : files) 709 { 710 valueStrings.add(f.getAbsolutePath()); 711 } 712 return Collections.unmodifiableList(valueStrings); 713 } 714 715 716 717 /** 718 * {@inheritDoc} 719 */ 720 @Override() 721 protected boolean hasDefaultValue() 722 { 723 return ((defaultValues != null) && (! defaultValues.isEmpty())); 724 } 725 726 727 728 /** 729 * {@inheritDoc} 730 */ 731 @Override() 732 public String getDataTypeName() 733 { 734 if (mustBeDirectory) 735 { 736 return INFO_FILE_TYPE_PATH_DIRECTORY.get(); 737 } 738 else 739 { 740 return INFO_FILE_TYPE_PATH_FILE.get(); 741 } 742 } 743 744 745 746 /** 747 * {@inheritDoc} 748 */ 749 @Override() 750 public String getValueConstraints() 751 { 752 final StringBuilder buffer = new StringBuilder(); 753 754 if (mustBeDirectory) 755 { 756 if (fileMustExist) 757 { 758 buffer.append(INFO_FILE_CONSTRAINTS_DIR_MUST_EXIST.get()); 759 } 760 else if (parentMustExist) 761 { 762 buffer.append(INFO_FILE_CONSTRAINTS_DIR_PARENT_MUST_EXIST.get()); 763 } 764 else 765 { 766 buffer.append(INFO_FILE_CONSTRAINTS_DIR_MAY_EXIST.get()); 767 } 768 } 769 else 770 { 771 if (fileMustExist) 772 { 773 buffer.append(INFO_FILE_CONSTRAINTS_FILE_MUST_EXIST.get()); 774 } 775 else if (parentMustExist) 776 { 777 buffer.append(INFO_FILE_CONSTRAINTS_FILE_PARENT_MUST_EXIST.get()); 778 } 779 else 780 { 781 buffer.append(INFO_FILE_CONSTRAINTS_FILE_MAY_EXIST.get()); 782 } 783 } 784 785 if (relativeBaseDirectory != null) 786 { 787 buffer.append(" "); 788 buffer.append(INFO_FILE_CONSTRAINTS_RELATIVE_PATH_SPECIFIED_ROOT.get( 789 relativeBaseDirectory.getAbsolutePath())); 790 } 791 792 return buffer.toString(); 793 } 794 795 796 797 /** 798 * {@inheritDoc} 799 */ 800 @Override() 801 protected void reset() 802 { 803 super.reset(); 804 values.clear(); 805 } 806 807 808 809 /** 810 * {@inheritDoc} 811 */ 812 @Override() 813 public FileArgument getCleanCopy() 814 { 815 return new FileArgument(this); 816 } 817 818 819 820 /** 821 * {@inheritDoc} 822 */ 823 @Override() 824 protected void addToCommandLine(final List<String> argStrings) 825 { 826 if (values != null) 827 { 828 for (final File f : values) 829 { 830 argStrings.add(getIdentifierString()); 831 if (isSensitive()) 832 { 833 argStrings.add("***REDACTED***"); 834 } 835 else 836 { 837 argStrings.add(f.getAbsolutePath()); 838 } 839 } 840 } 841 } 842 843 844 845 /** 846 * {@inheritDoc} 847 */ 848 @Override() 849 public void toString(final StringBuilder buffer) 850 { 851 buffer.append("FileArgument("); 852 appendBasicToStringInfo(buffer); 853 854 buffer.append(", fileMustExist="); 855 buffer.append(fileMustExist); 856 buffer.append(", parentMustExist="); 857 buffer.append(parentMustExist); 858 buffer.append(", mustBeFile="); 859 buffer.append(mustBeFile); 860 buffer.append(", mustBeDirectory="); 861 buffer.append(mustBeDirectory); 862 863 if (relativeBaseDirectory != null) 864 { 865 buffer.append(", relativeBaseDirectory='"); 866 buffer.append(relativeBaseDirectory.getAbsolutePath()); 867 buffer.append('\''); 868 } 869 870 if ((defaultValues != null) && (! defaultValues.isEmpty())) 871 { 872 if (defaultValues.size() == 1) 873 { 874 buffer.append(", defaultValue='"); 875 buffer.append(defaultValues.get(0).toString()); 876 } 877 else 878 { 879 buffer.append(", defaultValues={"); 880 881 final Iterator<File> iterator = defaultValues.iterator(); 882 while (iterator.hasNext()) 883 { 884 buffer.append('\''); 885 buffer.append(iterator.next().toString()); 886 buffer.append('\''); 887 888 if (iterator.hasNext()) 889 { 890 buffer.append(", "); 891 } 892 } 893 894 buffer.append('}'); 895 } 896 } 897 898 buffer.append(')'); 899 } 900}