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