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