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; 022 023 024 025import java.io.File; 026import java.io.FileOutputStream; 027import java.io.OutputStream; 028import java.io.PrintStream; 029import java.util.Collections; 030import java.util.Iterator; 031import java.util.LinkedHashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036import java.util.TreeMap; 037import java.util.concurrent.atomic.AtomicReference; 038 039import com.unboundid.ldap.sdk.LDAPException; 040import com.unboundid.ldap.sdk.ResultCode; 041import com.unboundid.util.args.ArgumentException; 042import com.unboundid.util.args.ArgumentParser; 043import com.unboundid.util.args.BooleanArgument; 044import com.unboundid.util.args.FileArgument; 045import com.unboundid.util.args.SubCommand; 046 047import static com.unboundid.util.Debug.*; 048import static com.unboundid.util.StaticUtils.*; 049import static com.unboundid.util.UtilityMessages.*; 050 051 052 053/** 054 * This class provides a framework for developing command-line tools that use 055 * the argument parser provided as part of the UnboundID LDAP SDK for Java. 056 * This tool adds a "-H" or "--help" option, which can be used to display usage 057 * information for the program, and may also add a "-V" or "--version" option, 058 * which can display the tool version. 059 * <BR><BR> 060 * Subclasses should include their own {@code main} method that creates an 061 * instance of a {@code CommandLineTool} and should invoke the 062 * {@link CommandLineTool#runTool} method with the provided arguments. For 063 * example: 064 * <PRE> 065 * public class ExampleCommandLineTool 066 * extends CommandLineTool 067 * { 068 * public static void main(String[] args) 069 * { 070 * ExampleCommandLineTool tool = new ExampleCommandLineTool(); 071 * ResultCode resultCode = tool.runTool(args); 072 * if (resultCode != ResultCode.SUCCESS) 073 * { 074 * System.exit(resultCode.intValue()); 075 * } 076 * | 077 * 078 * public ExampleCommandLineTool() 079 * { 080 * super(System.out, System.err); 081 * } 082 * 083 * // The rest of the tool implementation goes here. 084 * ... 085 * } 086 * </PRE>. 087 * <BR><BR> 088 * Note that in general, methods in this class are not threadsafe. However, the 089 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked 090 * concurrently by any number of threads. 091 */ 092@Extensible() 093@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 094public abstract class CommandLineTool 095{ 096 // The print stream that was originally used for standard output. It may not 097 // be the current standard output stream if an output file has been 098 // configured. 099 private final PrintStream originalOut; 100 101 // The print stream that was originally used for standard error. It may not 102 // be the current standard error stream if an output file has been configured. 103 private final PrintStream originalErr; 104 105 // The print stream to use for messages written to standard output. 106 private volatile PrintStream out; 107 108 // The print stream to use for messages written to standard error. 109 private volatile PrintStream err; 110 111 // The argument used to indicate that the tool should append to the output 112 // file rather than overwrite it. 113 private BooleanArgument appendToOutputFileArgument = null; 114 115 // The argument used to request tool help. 116 private BooleanArgument helpArgument = null; 117 118 // The argument used to request help about SASL authentication. 119 private BooleanArgument helpSASLArgument = null; 120 121 // The argument used to request help information about all of the subcommands. 122 private BooleanArgument helpSubcommandsArgument = null; 123 124 // The argument used to request interactive mode. 125 private BooleanArgument interactiveArgument = null; 126 127 // The argument used to indicate that output should be written to standard out 128 // as well as the specified output file. 129 private BooleanArgument teeOutputArgument = null; 130 131 // The argument used to request the tool version. 132 private BooleanArgument versionArgument = null; 133 134 // The argument used to specify the output file for standard output and 135 // standard error. 136 private FileArgument outputFileArgument = null; 137 138 139 140 /** 141 * Creates a new instance of this command-line tool with the provided 142 * information. 143 * 144 * @param outStream The output stream to use for standard output. It may be 145 * {@code System.out} for the JVM's default standard output 146 * stream, {@code null} if no output should be generated, 147 * or a custom output stream if the output should be sent 148 * to an alternate location. 149 * @param errStream The output stream to use for standard error. It may be 150 * {@code System.err} for the JVM's default standard error 151 * stream, {@code null} if no output should be generated, 152 * or a custom output stream if the output should be sent 153 * to an alternate location. 154 */ 155 public CommandLineTool(final OutputStream outStream, 156 final OutputStream errStream) 157 { 158 if (outStream == null) 159 { 160 out = NullOutputStream.getPrintStream(); 161 } 162 else 163 { 164 out = new PrintStream(outStream); 165 } 166 167 if (errStream == null) 168 { 169 err = NullOutputStream.getPrintStream(); 170 } 171 else 172 { 173 err = new PrintStream(errStream); 174 } 175 176 originalOut = out; 177 originalErr = err; 178 } 179 180 181 182 /** 183 * Performs all processing for this command-line tool. This includes: 184 * <UL> 185 * <LI>Creating the argument parser and populating it using the 186 * {@link #addToolArguments} method.</LI> 187 * <LI>Parsing the provided set of command line arguments, including any 188 * additional validation using the {@link #doExtendedArgumentValidation} 189 * method.</LI> 190 * <LI>Invoking the {@link #doToolProcessing} method to do the appropriate 191 * work for this tool.</LI> 192 * </UL> 193 * 194 * @param args The command-line arguments provided to this program. 195 * 196 * @return The result of processing this tool. It should be 197 * {@link ResultCode#SUCCESS} if the tool completed its work 198 * successfully, or some other result if a problem occurred. 199 */ 200 public final ResultCode runTool(final String... args) 201 { 202 final ArgumentParser parser; 203 try 204 { 205 parser = createArgumentParser(); 206 if (supportsInteractiveMode() && defaultsToInteractiveMode() && 207 ((args == null) || (args.length == 0))) 208 { 209 // We'll skip argument parsing in this case because no arguments were 210 // provided, and the tool may not allow no arguments to be provided in 211 // non-interactive mode. 212 } 213 else 214 { 215 parser.parse(args); 216 } 217 218 final File generatedPropertiesFile = parser.getGeneratedPropertiesFile(); 219 if (supportsPropertiesFile() && (generatedPropertiesFile != null)) 220 { 221 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1, 222 INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get( 223 generatedPropertiesFile.getAbsolutePath())); 224 return ResultCode.SUCCESS; 225 } 226 227 if (helpArgument.isPresent()) 228 { 229 out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 230 displayExampleUsages(parser); 231 return ResultCode.SUCCESS; 232 } 233 234 if ((helpSASLArgument != null) && helpSASLArgument.isPresent()) 235 { 236 out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 237 return ResultCode.SUCCESS; 238 } 239 240 if ((helpSubcommandsArgument != null) && 241 helpSubcommandsArgument.isPresent()) 242 { 243 final TreeMap<String,SubCommand> subCommands = 244 getSortedSubCommands(parser); 245 for (final SubCommand sc : subCommands.values()) 246 { 247 final StringBuilder nameBuffer = new StringBuilder(); 248 249 final Iterator<String> nameIterator = sc.getNames().iterator(); 250 while (nameIterator.hasNext()) 251 { 252 nameBuffer.append(nameIterator.next()); 253 if (nameIterator.hasNext()) 254 { 255 nameBuffer.append(", "); 256 } 257 } 258 out(nameBuffer.toString()); 259 260 for (final String descriptionLine : 261 wrapLine(sc.getDescription(), 262 (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3))) 263 { 264 out(" " + descriptionLine); 265 } 266 out(); 267 } 268 269 wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1), 270 INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName())); 271 return ResultCode.SUCCESS; 272 } 273 274 if ((versionArgument != null) && versionArgument.isPresent()) 275 { 276 out(getToolVersion()); 277 return ResultCode.SUCCESS; 278 } 279 280 boolean extendedValidationDone = false; 281 if (interactiveArgument != null) 282 { 283 if (interactiveArgument.isPresent() || 284 (defaultsToInteractiveMode() && 285 ((args == null) || (args.length == 0)))) 286 { 287 final CommandLineToolInteractiveModeProcessor interactiveProcessor = 288 new CommandLineToolInteractiveModeProcessor(this, parser); 289 try 290 { 291 interactiveProcessor.doInteractiveModeProcessing(); 292 extendedValidationDone = true; 293 } 294 catch (final LDAPException le) 295 { 296 debugException(le); 297 298 final String message = le.getMessage(); 299 if ((message != null) && (message.length() > 0)) 300 { 301 err(message); 302 } 303 304 return le.getResultCode(); 305 } 306 } 307 } 308 309 if (! extendedValidationDone) 310 { 311 doExtendedArgumentValidation(); 312 } 313 } 314 catch (ArgumentException ae) 315 { 316 debugException(ae); 317 err(ae.getMessage()); 318 return ResultCode.PARAM_ERROR; 319 } 320 321 if ((outputFileArgument != null) && outputFileArgument.isPresent()) 322 { 323 final File outputFile = outputFileArgument.getValue(); 324 final boolean append = ((appendToOutputFileArgument != null) && 325 appendToOutputFileArgument.isPresent()); 326 327 final PrintStream outputFileStream; 328 try 329 { 330 final FileOutputStream fos = new FileOutputStream(outputFile, append); 331 outputFileStream = new PrintStream(fos, true, "UTF-8"); 332 } 333 catch (final Exception e) 334 { 335 debugException(e); 336 err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get( 337 outputFile.getAbsolutePath(), getExceptionMessage(e))); 338 return ResultCode.LOCAL_ERROR; 339 } 340 341 if ((teeOutputArgument != null) && teeOutputArgument.isPresent()) 342 { 343 out = new PrintStream(new TeeOutputStream(out, outputFileStream)); 344 err = new PrintStream(new TeeOutputStream(err, outputFileStream)); 345 } 346 else 347 { 348 out = outputFileStream; 349 err = outputFileStream; 350 } 351 } 352 353 354 // If any values were selected using a properties file, then display 355 // information about them. 356 final List<String> argsSetFromPropertiesFiles = 357 parser.getArgumentsSetFromPropertiesFile(); 358 if (! argsSetFromPropertiesFiles.isEmpty()) 359 { 360 for (final String line : 361 wrapLine( 362 INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get( 363 parser.getPropertiesFileUsed().getPath()), 364 (TERMINAL_WIDTH_COLUMNS - 3))) 365 { 366 out("# ", line); 367 } 368 369 final StringBuilder buffer = new StringBuilder(); 370 for (final String s : argsSetFromPropertiesFiles) 371 { 372 if (s.startsWith("-")) 373 { 374 if (buffer.length() > 0) 375 { 376 out(buffer); 377 buffer.setLength(0); 378 } 379 380 buffer.append("# "); 381 buffer.append(s); 382 } 383 else 384 { 385 if (buffer.length() == 0) 386 { 387 // This should never happen. 388 buffer.append("# "); 389 } 390 else 391 { 392 buffer.append(' '); 393 } 394 395 buffer.append(StaticUtils.cleanExampleCommandLineArgument(s)); 396 } 397 } 398 399 if (buffer.length() > 0) 400 { 401 out(buffer); 402 } 403 404 out(); 405 } 406 407 408 CommandLineToolShutdownHook shutdownHook = null; 409 final AtomicReference<ResultCode> exitCode = 410 new AtomicReference<ResultCode>(); 411 if (registerShutdownHook()) 412 { 413 shutdownHook = new CommandLineToolShutdownHook(this, exitCode); 414 Runtime.getRuntime().addShutdownHook(shutdownHook); 415 } 416 417 try 418 { 419 exitCode.set(doToolProcessing()); 420 } 421 catch (Exception e) 422 { 423 debugException(e); 424 err(getExceptionMessage(e)); 425 exitCode.set(ResultCode.LOCAL_ERROR); 426 } 427 finally 428 { 429 if (shutdownHook != null) 430 { 431 Runtime.getRuntime().removeShutdownHook(shutdownHook); 432 } 433 } 434 435 return exitCode.get(); 436 } 437 438 439 440 /** 441 * Retrieves a sorted map of subcommands for the provided argument parser, 442 * alphabetized by primary name. 443 * 444 * @param parser The argument parser for which to get the sorted 445 * subcommands. 446 * 447 * @return The sorted map of subcommands. 448 */ 449 private static TreeMap<String,SubCommand> getSortedSubCommands( 450 final ArgumentParser parser) 451 { 452 final TreeMap<String,SubCommand> m = new TreeMap<String,SubCommand>(); 453 for (final SubCommand sc : parser.getSubCommands()) 454 { 455 m.put(sc.getPrimaryName(), sc); 456 } 457 return m; 458 } 459 460 461 462 /** 463 * Writes example usage information for this tool to the standard output 464 * stream. 465 * 466 * @param parser The argument parser used to process the provided set of 467 * command-line arguments. 468 */ 469 private void displayExampleUsages(final ArgumentParser parser) 470 { 471 final LinkedHashMap<String[],String> examples; 472 if ((parser != null) && (parser.getSelectedSubCommand() != null)) 473 { 474 examples = parser.getSelectedSubCommand().getExampleUsages(); 475 } 476 else 477 { 478 examples = getExampleUsages(); 479 } 480 481 if ((examples == null) || examples.isEmpty()) 482 { 483 return; 484 } 485 486 out(INFO_CL_TOOL_LABEL_EXAMPLES); 487 488 final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 489 for (final Map.Entry<String[],String> e : examples.entrySet()) 490 { 491 out(); 492 wrapOut(2, wrapWidth, e.getValue()); 493 out(); 494 495 final StringBuilder buffer = new StringBuilder(); 496 buffer.append(" "); 497 buffer.append(getToolName()); 498 499 final String[] args = e.getKey(); 500 for (int i=0; i < args.length; i++) 501 { 502 buffer.append(' '); 503 504 // If the argument has a value, then make sure to keep it on the same 505 // line as the argument name. This may introduce false positives due to 506 // unnamed trailing arguments, but the worst that will happen that case 507 // is that the output may be wrapped earlier than necessary one time. 508 String arg = args[i]; 509 if (arg.startsWith("-")) 510 { 511 if ((i < (args.length - 1)) && (! args[i+1].startsWith("-"))) 512 { 513 ExampleCommandLineArgument cleanArg = 514 ExampleCommandLineArgument.getCleanArgument(args[i+1]); 515 arg += ' ' + cleanArg.getLocalForm(); 516 i++; 517 } 518 } 519 else 520 { 521 ExampleCommandLineArgument cleanArg = 522 ExampleCommandLineArgument.getCleanArgument(arg); 523 arg = cleanArg.getLocalForm(); 524 } 525 526 if ((buffer.length() + arg.length() + 2) < wrapWidth) 527 { 528 buffer.append(arg); 529 } 530 else 531 { 532 buffer.append('\\'); 533 out(buffer.toString()); 534 buffer.setLength(0); 535 buffer.append(" "); 536 buffer.append(arg); 537 } 538 } 539 540 out(buffer.toString()); 541 } 542 } 543 544 545 546 /** 547 * Retrieves the name of this tool. It should be the name of the command used 548 * to invoke this tool. 549 * 550 * @return The name for this tool. 551 */ 552 public abstract String getToolName(); 553 554 555 556 /** 557 * Retrieves a human-readable description for this tool. 558 * 559 * @return A human-readable description for this tool. 560 */ 561 public abstract String getToolDescription(); 562 563 564 565 /** 566 * Retrieves a version string for this tool, if available. 567 * 568 * @return A version string for this tool, or {@code null} if none is 569 * available. 570 */ 571 public String getToolVersion() 572 { 573 return null; 574 } 575 576 577 578 /** 579 * Retrieves the minimum number of unnamed trailing arguments that must be 580 * provided for this tool. If a tool requires the use of trailing arguments, 581 * then it must override this method and the {@link #getMaxTrailingArguments} 582 * arguments to return nonzero values, and it must also override the 583 * {@link #getTrailingArgumentsPlaceholder} method to return a 584 * non-{@code null} value. 585 * 586 * @return The minimum number of unnamed trailing arguments that may be 587 * provided for this tool. A value of zero indicates that the tool 588 * may be invoked without any trailing arguments. 589 */ 590 public int getMinTrailingArguments() 591 { 592 return 0; 593 } 594 595 596 597 /** 598 * Retrieves the maximum number of unnamed trailing arguments that may be 599 * provided for this tool. If a tool supports trailing arguments, then it 600 * must override this method to return a nonzero value, and must also override 601 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to 602 * return a non-{@code null} value. 603 * 604 * @return The maximum number of unnamed trailing arguments that may be 605 * provided for this tool. A value of zero indicates that trailing 606 * arguments are not allowed. A negative value indicates that there 607 * should be no limit on the number of trailing arguments. 608 */ 609 public int getMaxTrailingArguments() 610 { 611 return 0; 612 } 613 614 615 616 /** 617 * Retrieves a placeholder string that should be used for trailing arguments 618 * in the usage information for this tool. 619 * 620 * @return A placeholder string that should be used for trailing arguments in 621 * the usage information for this tool, or {@code null} if trailing 622 * arguments are not supported. 623 */ 624 public String getTrailingArgumentsPlaceholder() 625 { 626 return null; 627 } 628 629 630 631 /** 632 * Indicates whether this tool should provide support for an interactive mode, 633 * in which the tool offers a mode in which the arguments can be provided in 634 * a text-driven menu rather than requiring them to be given on the command 635 * line. If interactive mode is supported, it may be invoked using the 636 * "--interactive" argument. Alternately, if interactive mode is supported 637 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 638 * interactive mode may be invoked by simply launching the tool without any 639 * arguments. 640 * 641 * @return {@code true} if this tool supports interactive mode, or 642 * {@code false} if not. 643 */ 644 public boolean supportsInteractiveMode() 645 { 646 return false; 647 } 648 649 650 651 /** 652 * Indicates whether this tool defaults to launching in interactive mode if 653 * the tool is invoked without any command-line arguments. This will only be 654 * used if {@link #supportsInteractiveMode()} returns {@code true}. 655 * 656 * @return {@code true} if this tool defaults to using interactive mode if 657 * launched without any command-line arguments, or {@code false} if 658 * not. 659 */ 660 public boolean defaultsToInteractiveMode() 661 { 662 return false; 663 } 664 665 666 667 /** 668 * Indicates whether this tool supports the use of a properties file for 669 * specifying default values for arguments that aren't specified on the 670 * command line. 671 * 672 * @return {@code true} if this tool supports the use of a properties file 673 * for specifying default values for arguments that aren't specified 674 * on the command line, or {@code false} if not. 675 */ 676 public boolean supportsPropertiesFile() 677 { 678 return false; 679 } 680 681 682 683 /** 684 * Indicates whether this tool should provide arguments for redirecting output 685 * to a file. If this method returns {@code true}, then the tool will offer 686 * an "--outputFile" argument that will specify the path to a file to which 687 * all standard output and standard error content will be written, and it will 688 * also offer a "--teeToStandardOut" argument that can only be used if the 689 * "--outputFile" argument is present and will cause all output to be written 690 * to both the specified output file and to standard output. 691 * 692 * @return {@code true} if this tool should provide arguments for redirecting 693 * output to a file, or {@code false} if not. 694 */ 695 protected boolean supportsOutputFile() 696 { 697 return false; 698 } 699 700 701 702 /** 703 * Creates a parser that can be used to to parse arguments accepted by 704 * this tool. 705 * 706 * @return ArgumentParser that can be used to parse arguments for this 707 * tool. 708 * 709 * @throws ArgumentException If there was a problem initializing the 710 * parser for this tool. 711 */ 712 public final ArgumentParser createArgumentParser() 713 throws ArgumentException 714 { 715 final ArgumentParser parser = new ArgumentParser(getToolName(), 716 getToolDescription(), getMinTrailingArguments(), 717 getMaxTrailingArguments(), getTrailingArgumentsPlaceholder()); 718 719 addToolArguments(parser); 720 721 if (supportsInteractiveMode()) 722 { 723 interactiveArgument = new BooleanArgument(null, "interactive", 724 INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get()); 725 interactiveArgument.setUsageArgument(true); 726 parser.addArgument(interactiveArgument); 727 } 728 729 if (supportsOutputFile()) 730 { 731 outputFileArgument = new FileArgument(null, "outputFile", false, 1, null, 732 INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true, 733 false); 734 outputFileArgument.addLongIdentifier("output-file"); 735 outputFileArgument.setUsageArgument(true); 736 parser.addArgument(outputFileArgument); 737 738 appendToOutputFileArgument = new BooleanArgument(null, 739 "appendToOutputFile", 1, 740 INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get( 741 outputFileArgument.getIdentifierString())); 742 appendToOutputFileArgument.addLongIdentifier("append-to-output-file"); 743 appendToOutputFileArgument.setUsageArgument(true); 744 parser.addArgument(appendToOutputFileArgument); 745 746 teeOutputArgument = new BooleanArgument(null, "teeOutput", 1, 747 INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get( 748 outputFileArgument.getIdentifierString())); 749 teeOutputArgument.addLongIdentifier("tee-output"); 750 teeOutputArgument.setUsageArgument(true); 751 parser.addArgument(teeOutputArgument); 752 753 parser.addDependentArgumentSet(appendToOutputFileArgument, 754 outputFileArgument); 755 parser.addDependentArgumentSet(teeOutputArgument, 756 outputFileArgument); 757 } 758 759 helpArgument = new BooleanArgument('H', "help", 760 INFO_CL_TOOL_DESCRIPTION_HELP.get()); 761 helpArgument.addShortIdentifier('?'); 762 helpArgument.setUsageArgument(true); 763 parser.addArgument(helpArgument); 764 765 if (! parser.getSubCommands().isEmpty()) 766 { 767 helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1, 768 INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get()); 769 helpSubcommandsArgument.addLongIdentifier("help-subcommands"); 770 helpSubcommandsArgument.setUsageArgument(true); 771 parser.addArgument(helpSubcommandsArgument); 772 } 773 774 final String version = getToolVersion(); 775 if ((version != null) && (version.length() > 0) && 776 (parser.getNamedArgument("version") == null)) 777 { 778 final Character shortIdentifier; 779 if (parser.getNamedArgument('V') == null) 780 { 781 shortIdentifier = 'V'; 782 } 783 else 784 { 785 shortIdentifier = null; 786 } 787 788 versionArgument = new BooleanArgument(shortIdentifier, "version", 789 INFO_CL_TOOL_DESCRIPTION_VERSION.get()); 790 versionArgument.setUsageArgument(true); 791 parser.addArgument(versionArgument); 792 } 793 794 if (supportsPropertiesFile()) 795 { 796 parser.enablePropertiesFileSupport(); 797 } 798 799 return parser; 800 } 801 802 803 804 /** 805 * Specifies the argument that is used to retrieve usage information about 806 * SASL authentication. 807 * 808 * @param helpSASLArgument The argument that is used to retrieve usage 809 * information about SASL authentication. 810 */ 811 void setHelpSASLArgument(final BooleanArgument helpSASLArgument) 812 { 813 this.helpSASLArgument = helpSASLArgument; 814 } 815 816 817 818 /** 819 * Retrieves a set containing the long identifiers used for usage arguments 820 * injected by this class. 821 * 822 * @param tool The tool to use to help make the determination. 823 * 824 * @return A set containing the long identifiers used for usage arguments 825 * injected by this class. 826 */ 827 static Set<String> getUsageArgumentIdentifiers(final CommandLineTool tool) 828 { 829 final LinkedHashSet<String> ids = new LinkedHashSet<String>(9); 830 831 ids.add("help"); 832 ids.add("version"); 833 ids.add("helpSubcommands"); 834 835 if (tool.supportsInteractiveMode()) 836 { 837 ids.add("interactive"); 838 } 839 840 if (tool.supportsPropertiesFile()) 841 { 842 ids.add("propertiesFilePath"); 843 ids.add("generatePropertiesFile"); 844 ids.add("noPropertiesFile"); 845 } 846 847 if (tool.supportsOutputFile()) 848 { 849 ids.add("outputFile"); 850 ids.add("appendToOutputFile"); 851 ids.add("teeOutput"); 852 } 853 854 return Collections.unmodifiableSet(ids); 855 } 856 857 858 859 /** 860 * Adds the command-line arguments supported for use with this tool to the 861 * provided argument parser. The tool may need to retain references to the 862 * arguments (and/or the argument parser, if trailing arguments are allowed) 863 * to it in order to obtain their values for use in later processing. 864 * 865 * @param parser The argument parser to which the arguments are to be added. 866 * 867 * @throws ArgumentException If a problem occurs while adding any of the 868 * tool-specific arguments to the provided 869 * argument parser. 870 */ 871 public abstract void addToolArguments(final ArgumentParser parser) 872 throws ArgumentException; 873 874 875 876 /** 877 * Performs any necessary processing that should be done to ensure that the 878 * provided set of command-line arguments were valid. This method will be 879 * called after the basic argument parsing has been performed and immediately 880 * before the {@link CommandLineTool#doToolProcessing} method is invoked. 881 * Note that if the tool supports interactive mode, then this method may be 882 * invoked multiple times to allow the user to interactively fix validation 883 * errors. 884 * 885 * @throws ArgumentException If there was a problem with the command-line 886 * arguments provided to this program. 887 */ 888 public void doExtendedArgumentValidation() 889 throws ArgumentException 890 { 891 // No processing will be performed by default. 892 } 893 894 895 896 /** 897 * Performs the core set of processing for this tool. 898 * 899 * @return A result code that indicates whether the processing completed 900 * successfully. 901 */ 902 public abstract ResultCode doToolProcessing(); 903 904 905 906 /** 907 * Indicates whether this tool should register a shutdown hook with the JVM. 908 * Shutdown hooks allow for a best-effort attempt to perform a specified set 909 * of processing when the JVM is shutting down under various conditions, 910 * including: 911 * <UL> 912 * <LI>When all non-daemon threads have stopped running (i.e., the tool has 913 * completed processing).</LI> 914 * <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI> 915 * <LI>When the JVM receives an external kill signal (e.g., via the use of 916 * the kill tool or interrupting the JVM with Ctrl+C).</LI> 917 * </UL> 918 * Shutdown hooks may not be invoked if the process is forcefully killed 919 * (e.g., using "kill -9", or the {@code System.halt()} or 920 * {@code Runtime.halt()} methods). 921 * <BR><BR> 922 * If this method is overridden to return {@code true}, then the 923 * {@link #doShutdownHookProcessing(ResultCode)} method should also be 924 * overridden to contain the logic that will be invoked when the JVM is 925 * shutting down in a manner that calls shutdown hooks. 926 * 927 * @return {@code true} if this tool should register a shutdown hook, or 928 * {@code false} if not. 929 */ 930 protected boolean registerShutdownHook() 931 { 932 return false; 933 } 934 935 936 937 /** 938 * Performs any processing that may be needed when the JVM is shutting down, 939 * whether because tool processing has completed or because it has been 940 * interrupted (e.g., by a kill or break signal). 941 * <BR><BR> 942 * Note that because shutdown hooks run at a delicate time in the life of the 943 * JVM, they should complete quickly and minimize access to external 944 * resources. See the documentation for the 945 * {@code java.lang.Runtime.addShutdownHook} method for recommendations and 946 * restrictions about writing shutdown hooks. 947 * 948 * @param resultCode The result code returned by the tool. It may be 949 * {@code null} if the tool was interrupted before it 950 * completed processing. 951 */ 952 protected void doShutdownHookProcessing(final ResultCode resultCode) 953 { 954 throw new LDAPSDKUsageException( 955 ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get( 956 getToolName())); 957 } 958 959 960 961 /** 962 * Retrieves a set of information that may be used to generate example usage 963 * information. Each element in the returned map should consist of a map 964 * between an example set of arguments and a string that describes the 965 * behavior of the tool when invoked with that set of arguments. 966 * 967 * @return A set of information that may be used to generate example usage 968 * information. It may be {@code null} or empty if no example usage 969 * information is available. 970 */ 971 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 972 public LinkedHashMap<String[],String> getExampleUsages() 973 { 974 return null; 975 } 976 977 978 979 /** 980 * Retrieves the print stream that will be used for standard output. 981 * 982 * @return The print stream that will be used for standard output. 983 */ 984 public final PrintStream getOut() 985 { 986 return out; 987 } 988 989 990 991 /** 992 * Retrieves the print stream that may be used to write to the original 993 * standard output. This may be different from the current standard output 994 * stream if an output file has been configured. 995 * 996 * @return The print stream that may be used to write to the original 997 * standard output. 998 */ 999 public final PrintStream getOriginalOut() 1000 { 1001 return originalOut; 1002 } 1003 1004 1005 1006 /** 1007 * Writes the provided message to the standard output stream for this tool. 1008 * <BR><BR> 1009 * This method is completely threadsafe and my be invoked concurrently by any 1010 * number of threads. 1011 * 1012 * @param msg The message components that will be written to the standard 1013 * output stream. They will be concatenated together on the same 1014 * line, and that line will be followed by an end-of-line 1015 * sequence. 1016 */ 1017 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1018 public final synchronized void out(final Object... msg) 1019 { 1020 write(out, 0, 0, msg); 1021 } 1022 1023 1024 1025 /** 1026 * Writes the provided message to the standard output stream for this tool, 1027 * optionally wrapping and/or indenting the text in the process. 1028 * <BR><BR> 1029 * This method is completely threadsafe and my be invoked concurrently by any 1030 * number of threads. 1031 * 1032 * @param indent The number of spaces each line should be indented. A 1033 * value less than or equal to zero indicates that no 1034 * indent should be used. 1035 * @param wrapColumn The column at which to wrap long lines. A value less 1036 * than or equal to two indicates that no wrapping should 1037 * be performed. If both an indent and a wrap column are 1038 * to be used, then the wrap column must be greater than 1039 * the indent. 1040 * @param msg The message components that will be written to the 1041 * standard output stream. They will be concatenated 1042 * together on the same line, and that line will be 1043 * followed by an end-of-line sequence. 1044 */ 1045 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1046 public final synchronized void wrapOut(final int indent, final int wrapColumn, 1047 final Object... msg) 1048 { 1049 write(out, indent, wrapColumn, msg); 1050 } 1051 1052 1053 1054 /** 1055 * Writes the provided message to the standard output stream for this tool, 1056 * optionally wrapping and/or indenting the text in the process. 1057 * <BR><BR> 1058 * This method is completely threadsafe and my be invoked concurrently by any 1059 * number of threads. 1060 * 1061 * @param firstLineIndent The number of spaces the first line should be 1062 * indented. A value less than or equal to zero 1063 * indicates that no indent should be used. 1064 * @param subsequentLineIndent The number of spaces each line except the 1065 * first should be indented. A value less than 1066 * or equal to zero indicates that no indent 1067 * should be used. 1068 * @param wrapColumn The column at which to wrap long lines. A 1069 * value less than or equal to two indicates 1070 * that no wrapping should be performed. If 1071 * both an indent and a wrap column are to be 1072 * used, then the wrap column must be greater 1073 * than the indent. 1074 * @param endWithNewline Indicates whether a newline sequence should 1075 * follow the last line that is printed. 1076 * @param msg The message components that will be written 1077 * to the standard output stream. They will be 1078 * concatenated together on the same line, and 1079 * that line will be followed by an end-of-line 1080 * sequence. 1081 */ 1082 final synchronized void wrapStandardOut(final int firstLineIndent, 1083 final int subsequentLineIndent, 1084 final int wrapColumn, 1085 final boolean endWithNewline, 1086 final Object... msg) 1087 { 1088 write(out, firstLineIndent, subsequentLineIndent, wrapColumn, 1089 endWithNewline, msg); 1090 } 1091 1092 1093 1094 /** 1095 * Retrieves the print stream that will be used for standard error. 1096 * 1097 * @return The print stream that will be used for standard error. 1098 */ 1099 public final PrintStream getErr() 1100 { 1101 return err; 1102 } 1103 1104 1105 1106 /** 1107 * Retrieves the print stream that may be used to write to the original 1108 * standard error. This may be different from the current standard error 1109 * stream if an output file has been configured. 1110 * 1111 * @return The print stream that may be used to write to the original 1112 * standard error. 1113 */ 1114 public final PrintStream getOriginalErr() 1115 { 1116 return originalErr; 1117 } 1118 1119 1120 1121 /** 1122 * Writes the provided message to the standard error stream for this tool. 1123 * <BR><BR> 1124 * This method is completely threadsafe and my be invoked concurrently by any 1125 * number of threads. 1126 * 1127 * @param msg The message components that will be written to the standard 1128 * error stream. They will be concatenated together on the same 1129 * line, and that line will be followed by an end-of-line 1130 * sequence. 1131 */ 1132 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1133 public final synchronized void err(final Object... msg) 1134 { 1135 write(err, 0, 0, msg); 1136 } 1137 1138 1139 1140 /** 1141 * Writes the provided message to the standard error stream for this tool, 1142 * optionally wrapping and/or indenting the text in the process. 1143 * <BR><BR> 1144 * This method is completely threadsafe and my be invoked concurrently by any 1145 * number of threads. 1146 * 1147 * @param indent The number of spaces each line should be indented. A 1148 * value less than or equal to zero indicates that no 1149 * indent should be used. 1150 * @param wrapColumn The column at which to wrap long lines. A value less 1151 * than or equal to two indicates that no wrapping should 1152 * be performed. If both an indent and a wrap column are 1153 * to be used, then the wrap column must be greater than 1154 * the indent. 1155 * @param msg The message components that will be written to the 1156 * standard output stream. They will be concatenated 1157 * together on the same line, and that line will be 1158 * followed by an end-of-line sequence. 1159 */ 1160 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1161 public final synchronized void wrapErr(final int indent, final int wrapColumn, 1162 final Object... msg) 1163 { 1164 write(err, indent, wrapColumn, msg); 1165 } 1166 1167 1168 1169 /** 1170 * Writes the provided message to the given print stream, optionally wrapping 1171 * and/or indenting the text in the process. 1172 * 1173 * @param stream The stream to which the message should be written. 1174 * @param indent The number of spaces each line should be indented. A 1175 * value less than or equal to zero indicates that no 1176 * indent should be used. 1177 * @param wrapColumn The column at which to wrap long lines. A value less 1178 * than or equal to two indicates that no wrapping should 1179 * be performed. If both an indent and a wrap column are 1180 * to be used, then the wrap column must be greater than 1181 * the indent. 1182 * @param msg The message components that will be written to the 1183 * standard output stream. They will be concatenated 1184 * together on the same line, and that line will be 1185 * followed by an end-of-line sequence. 1186 */ 1187 private static void write(final PrintStream stream, final int indent, 1188 final int wrapColumn, final Object... msg) 1189 { 1190 write(stream, indent, indent, wrapColumn, true, msg); 1191 } 1192 1193 1194 1195 /** 1196 * Writes the provided message to the given print stream, optionally wrapping 1197 * and/or indenting the text in the process. 1198 * 1199 * @param stream The stream to which the message should be 1200 * written. 1201 * @param firstLineIndent The number of spaces the first line should be 1202 * indented. A value less than or equal to zero 1203 * indicates that no indent should be used. 1204 * @param subsequentLineIndent The number of spaces all lines after the 1205 * first should be indented. A value less than 1206 * or equal to zero indicates that no indent 1207 * should be used. 1208 * @param wrapColumn The column at which to wrap long lines. A 1209 * value less than or equal to two indicates 1210 * that no wrapping should be performed. If 1211 * both an indent and a wrap column are to be 1212 * used, then the wrap column must be greater 1213 * than the indent. 1214 * @param endWithNewline Indicates whether a newline sequence should 1215 * follow the last line that is printed. 1216 * @param msg The message components that will be written 1217 * to the standard output stream. They will be 1218 * concatenated together on the same line, and 1219 * that line will be followed by an end-of-line 1220 * sequence. 1221 */ 1222 private static void write(final PrintStream stream, final int firstLineIndent, 1223 final int subsequentLineIndent, 1224 final int wrapColumn, 1225 final boolean endWithNewline, final Object... msg) 1226 { 1227 final StringBuilder buffer = new StringBuilder(); 1228 for (final Object o : msg) 1229 { 1230 buffer.append(o); 1231 } 1232 1233 if (wrapColumn > 2) 1234 { 1235 boolean firstLine = true; 1236 for (final String line : 1237 wrapLine(buffer.toString(), (wrapColumn - firstLineIndent), 1238 (wrapColumn - subsequentLineIndent))) 1239 { 1240 final int indent; 1241 if (firstLine) 1242 { 1243 indent = firstLineIndent; 1244 firstLine = false; 1245 } 1246 else 1247 { 1248 stream.println(); 1249 indent = subsequentLineIndent; 1250 } 1251 1252 if (indent > 0) 1253 { 1254 for (int i=0; i < indent; i++) 1255 { 1256 stream.print(' '); 1257 } 1258 } 1259 stream.print(line); 1260 } 1261 } 1262 else 1263 { 1264 if (firstLineIndent > 0) 1265 { 1266 for (int i=0; i < firstLineIndent; i++) 1267 { 1268 stream.print(' '); 1269 } 1270 } 1271 stream.print(buffer.toString()); 1272 } 1273 1274 if (endWithNewline) 1275 { 1276 stream.println(); 1277 } 1278 stream.flush(); 1279 } 1280}