001/* 002 * Copyright 2008-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2019 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util; 022 023 024 025import java.io.File; 026import java.io.FileOutputStream; 027import java.io.OutputStream; 028import java.io.PrintStream; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.HashSet; 032import java.util.Iterator; 033import java.util.LinkedHashMap; 034import java.util.LinkedHashSet; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038import java.util.TreeMap; 039import java.util.concurrent.atomic.AtomicReference; 040 041import com.unboundid.ldap.sdk.LDAPException; 042import com.unboundid.ldap.sdk.ResultCode; 043import com.unboundid.util.args.Argument; 044import com.unboundid.util.args.ArgumentException; 045import com.unboundid.util.args.ArgumentHelper; 046import com.unboundid.util.args.ArgumentParser; 047import com.unboundid.util.args.BooleanArgument; 048import com.unboundid.util.args.FileArgument; 049import com.unboundid.util.args.SubCommand; 050import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogger; 051import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogDetails; 052import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogShutdownHook; 053 054import static com.unboundid.util.UtilityMessages.*; 055 056 057 058/** 059 * This class provides a framework for developing command-line tools that use 060 * the argument parser provided as part of the UnboundID LDAP SDK for Java. 061 * This tool adds a "-H" or "--help" option, which can be used to display usage 062 * information for the program, and may also add a "-V" or "--version" option, 063 * which can display the tool version. 064 * <BR><BR> 065 * Subclasses should include their own {@code main} method that creates an 066 * instance of a {@code CommandLineTool} and should invoke the 067 * {@link CommandLineTool#runTool} method with the provided arguments. For 068 * example: 069 * <PRE> 070 * public class ExampleCommandLineTool 071 * extends CommandLineTool 072 * { 073 * public static void main(String[] args) 074 * { 075 * ExampleCommandLineTool tool = new ExampleCommandLineTool(); 076 * ResultCode resultCode = tool.runTool(args); 077 * if (resultCode != ResultCode.SUCCESS) 078 * { 079 * System.exit(resultCode.intValue()); 080 * } 081 * } 082 * 083 * public ExampleCommandLineTool() 084 * { 085 * super(System.out, System.err); 086 * } 087 * 088 * // The rest of the tool implementation goes here. 089 * ... 090 * } 091 * </PRE>. 092 * <BR><BR> 093 * Note that in general, methods in this class are not threadsafe. However, the 094 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked 095 * concurrently by any number of threads. 096 */ 097@Extensible() 098@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 099public abstract class CommandLineTool 100{ 101 // The argument used to indicate that the tool should append to the output 102 // file rather than overwrite it. 103 private BooleanArgument appendToOutputFileArgument = null; 104 105 // The argument used to request tool help. 106 private BooleanArgument helpArgument = null; 107 108 // The argument used to request help about SASL authentication. 109 private BooleanArgument helpSASLArgument = null; 110 111 // The argument used to request help information about all of the subcommands. 112 private BooleanArgument helpSubcommandsArgument = null; 113 114 // The argument used to request interactive mode. 115 private BooleanArgument interactiveArgument = null; 116 117 // The argument used to indicate that output should be written to standard out 118 // as well as the specified output file. 119 private BooleanArgument teeOutputArgument = null; 120 121 // The argument used to request the tool version. 122 private BooleanArgument versionArgument = null; 123 124 // The argument used to specify the output file for standard output and 125 // standard error. 126 private FileArgument outputFileArgument = null; 127 128 // A list of arguments that can be used to enable SSL/TLS debugging. 129 private final List<BooleanArgument> enableSSLDebuggingArguments; 130 131 // The password file reader for this tool. 132 private final PasswordFileReader passwordFileReader; 133 134 // The print stream that was originally used for standard output. It may not 135 // be the current standard output stream if an output file has been 136 // configured. 137 private final PrintStream originalOut; 138 139 // The print stream that was originally used for standard error. It may not 140 // be the current standard error stream if an output file has been configured. 141 private final PrintStream originalErr; 142 143 // The print stream to use for messages written to standard output. 144 private volatile PrintStream out; 145 146 // The print stream to use for messages written to standard error. 147 private volatile PrintStream err; 148 149 150 151 /** 152 * Creates a new instance of this command-line tool with the provided 153 * information. 154 * 155 * @param outStream The output stream to use for standard output. It may be 156 * {@code System.out} for the JVM's default standard output 157 * stream, {@code null} if no output should be generated, 158 * or a custom output stream if the output should be sent 159 * to an alternate location. 160 * @param errStream The output stream to use for standard error. It may be 161 * {@code System.err} for the JVM's default standard error 162 * stream, {@code null} if no output should be generated, 163 * or a custom output stream if the output should be sent 164 * to an alternate location. 165 */ 166 public CommandLineTool(final OutputStream outStream, 167 final OutputStream errStream) 168 { 169 if (outStream == null) 170 { 171 out = NullOutputStream.getPrintStream(); 172 } 173 else 174 { 175 out = new PrintStream(outStream); 176 } 177 178 if (errStream == null) 179 { 180 err = NullOutputStream.getPrintStream(); 181 } 182 else 183 { 184 err = new PrintStream(errStream); 185 } 186 187 originalOut = out; 188 originalErr = err; 189 190 passwordFileReader = new PasswordFileReader(out, err); 191 enableSSLDebuggingArguments = new ArrayList<>(1); 192 } 193 194 195 196 /** 197 * Performs all processing for this command-line tool. This includes: 198 * <UL> 199 * <LI>Creating the argument parser and populating it using the 200 * {@link #addToolArguments} method.</LI> 201 * <LI>Parsing the provided set of command line arguments, including any 202 * additional validation using the {@link #doExtendedArgumentValidation} 203 * method.</LI> 204 * <LI>Invoking the {@link #doToolProcessing} method to do the appropriate 205 * work for this tool.</LI> 206 * </UL> 207 * 208 * @param args The command-line arguments provided to this program. 209 * 210 * @return The result of processing this tool. It should be 211 * {@link ResultCode#SUCCESS} if the tool completed its work 212 * successfully, or some other result if a problem occurred. 213 */ 214 public final ResultCode runTool(final String... args) 215 { 216 final ArgumentParser parser; 217 try 218 { 219 parser = createArgumentParser(); 220 boolean exceptionFromParsingWithNoArgumentsExplicitlyProvided = false; 221 if (supportsInteractiveMode() && defaultsToInteractiveMode() && 222 ((args == null) || (args.length == 0))) 223 { 224 // We'll go ahead and perform argument parsing even though no arguments 225 // were provided because there might be a properties file that should 226 // prevent running in interactive mode. But we'll ignore any exception 227 // thrown during argument parsing because the tool might require 228 // arguments when run non-interactively. 229 try 230 { 231 parser.parse(args); 232 } 233 catch (final Exception e) 234 { 235 Debug.debugException(e); 236 exceptionFromParsingWithNoArgumentsExplicitlyProvided = true; 237 } 238 } 239 else 240 { 241 parser.parse(args); 242 } 243 244 final File generatedPropertiesFile = parser.getGeneratedPropertiesFile(); 245 if (supportsPropertiesFile() && (generatedPropertiesFile != null)) 246 { 247 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1, 248 INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get( 249 generatedPropertiesFile.getAbsolutePath())); 250 return ResultCode.SUCCESS; 251 } 252 253 if (helpArgument.isPresent()) 254 { 255 out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 256 displayExampleUsages(parser); 257 return ResultCode.SUCCESS; 258 } 259 260 if ((helpSASLArgument != null) && helpSASLArgument.isPresent()) 261 { 262 out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 263 return ResultCode.SUCCESS; 264 } 265 266 if ((helpSubcommandsArgument != null) && 267 helpSubcommandsArgument.isPresent()) 268 { 269 final TreeMap<String,SubCommand> subCommands = 270 getSortedSubCommands(parser); 271 for (final SubCommand sc : subCommands.values()) 272 { 273 final StringBuilder nameBuffer = new StringBuilder(); 274 275 final Iterator<String> nameIterator = sc.getNames(false).iterator(); 276 while (nameIterator.hasNext()) 277 { 278 nameBuffer.append(nameIterator.next()); 279 if (nameIterator.hasNext()) 280 { 281 nameBuffer.append(", "); 282 } 283 } 284 out(nameBuffer.toString()); 285 286 for (final String descriptionLine : 287 StaticUtils.wrapLine(sc.getDescription(), 288 (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3))) 289 { 290 out(" " + descriptionLine); 291 } 292 out(); 293 } 294 295 wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1), 296 INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName())); 297 return ResultCode.SUCCESS; 298 } 299 300 if ((versionArgument != null) && versionArgument.isPresent()) 301 { 302 out(getToolVersion()); 303 return ResultCode.SUCCESS; 304 } 305 306 // If we should enable SSL/TLS debugging, then do that now. Do it before 307 // any kind of user-defined validation is performed. Java is really 308 // touchy about when this is done, and we need to do it before any 309 // connection attempt is made. 310 for (final BooleanArgument a : enableSSLDebuggingArguments) 311 { 312 if (a.isPresent()) 313 { 314 StaticUtils.setSystemProperty("javax.net.debug", "all"); 315 } 316 } 317 318 boolean extendedValidationDone = false; 319 if (interactiveArgument != null) 320 { 321 if (interactiveArgument.isPresent() || 322 (defaultsToInteractiveMode() && 323 ((args == null) || (args.length == 0)) && 324 (parser.getArgumentsSetFromPropertiesFile().isEmpty() || 325 exceptionFromParsingWithNoArgumentsExplicitlyProvided))) 326 { 327 try 328 { 329 final List<String> interactiveArgs = 330 requestToolArgumentsInteractively(parser); 331 if (interactiveArgs == null) 332 { 333 final CommandLineToolInteractiveModeProcessor processor = 334 new CommandLineToolInteractiveModeProcessor(this, parser); 335 processor.doInteractiveModeProcessing(); 336 extendedValidationDone = true; 337 } 338 else 339 { 340 ArgumentHelper.reset(parser); 341 parser.parse(StaticUtils.toArray(interactiveArgs, String.class)); 342 } 343 } 344 catch (final LDAPException le) 345 { 346 Debug.debugException(le); 347 348 final String message = le.getMessage(); 349 if ((message != null) && (! message.isEmpty())) 350 { 351 err(message); 352 } 353 354 return le.getResultCode(); 355 } 356 } 357 } 358 359 if (! extendedValidationDone) 360 { 361 doExtendedArgumentValidation(); 362 } 363 } 364 catch (final ArgumentException ae) 365 { 366 Debug.debugException(ae); 367 err(ae.getMessage()); 368 return ResultCode.PARAM_ERROR; 369 } 370 371 if ((outputFileArgument != null) && outputFileArgument.isPresent()) 372 { 373 final File outputFile = outputFileArgument.getValue(); 374 final boolean append = ((appendToOutputFileArgument != null) && 375 appendToOutputFileArgument.isPresent()); 376 377 final PrintStream outputFileStream; 378 try 379 { 380 final FileOutputStream fos = new FileOutputStream(outputFile, append); 381 outputFileStream = new PrintStream(fos, true, "UTF-8"); 382 } 383 catch (final Exception e) 384 { 385 Debug.debugException(e); 386 err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get( 387 outputFile.getAbsolutePath(), StaticUtils.getExceptionMessage(e))); 388 return ResultCode.LOCAL_ERROR; 389 } 390 391 if ((teeOutputArgument != null) && teeOutputArgument.isPresent()) 392 { 393 out = new PrintStream(new TeeOutputStream(out, outputFileStream)); 394 err = new PrintStream(new TeeOutputStream(err, outputFileStream)); 395 } 396 else 397 { 398 out = outputFileStream; 399 err = outputFileStream; 400 } 401 } 402 403 404 // If any values were selected using a properties file, then display 405 // information about them. 406 final List<String> argsSetFromPropertiesFiles = 407 parser.getArgumentsSetFromPropertiesFile(); 408 if ((! argsSetFromPropertiesFiles.isEmpty()) && 409 (! parser.suppressPropertiesFileComment())) 410 { 411 for (final String line : 412 StaticUtils.wrapLine( 413 INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get( 414 parser.getPropertiesFileUsed().getPath()), 415 (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3))) 416 { 417 out("# ", line); 418 } 419 420 final StringBuilder buffer = new StringBuilder(); 421 for (final String s : argsSetFromPropertiesFiles) 422 { 423 if (s.startsWith("-")) 424 { 425 if (buffer.length() > 0) 426 { 427 out(buffer); 428 buffer.setLength(0); 429 } 430 431 buffer.append("# "); 432 buffer.append(s); 433 } 434 else 435 { 436 if (buffer.length() == 0) 437 { 438 // This should never happen. 439 buffer.append("# "); 440 } 441 else 442 { 443 buffer.append(' '); 444 } 445 446 buffer.append(StaticUtils.cleanExampleCommandLineArgument(s)); 447 } 448 } 449 450 if (buffer.length() > 0) 451 { 452 out(buffer); 453 } 454 455 out(); 456 } 457 458 459 CommandLineToolShutdownHook shutdownHook = null; 460 final AtomicReference<ResultCode> exitCode = new AtomicReference<>(); 461 if (registerShutdownHook()) 462 { 463 shutdownHook = new CommandLineToolShutdownHook(this, exitCode); 464 Runtime.getRuntime().addShutdownHook(shutdownHook); 465 } 466 467 final ToolInvocationLogDetails logDetails = 468 ToolInvocationLogger.getLogMessageDetails( 469 getToolName(), logToolInvocationByDefault(), getErr()); 470 ToolInvocationLogShutdownHook logShutdownHook = null; 471 472 if (logDetails.logInvocation()) 473 { 474 final HashSet<Argument> argumentsSetFromPropertiesFile = 475 new HashSet<>(StaticUtils.computeMapCapacity(10)); 476 final ArrayList<ObjectPair<String,String>> propertiesFileArgList = 477 new ArrayList<>(10); 478 getToolInvocationPropertiesFileArguments(parser, 479 argumentsSetFromPropertiesFile, propertiesFileArgList); 480 481 final ArrayList<ObjectPair<String,String>> providedArgList = 482 new ArrayList<>(10); 483 getToolInvocationProvidedArguments(parser, 484 argumentsSetFromPropertiesFile, providedArgList); 485 486 logShutdownHook = new ToolInvocationLogShutdownHook(logDetails); 487 Runtime.getRuntime().addShutdownHook(logShutdownHook); 488 489 final String propertiesFilePath; 490 if (propertiesFileArgList.isEmpty()) 491 { 492 propertiesFilePath = ""; 493 } 494 else 495 { 496 final File propertiesFile = parser.getPropertiesFileUsed(); 497 if (propertiesFile == null) 498 { 499 propertiesFilePath = ""; 500 } 501 else 502 { 503 propertiesFilePath = propertiesFile.getAbsolutePath(); 504 } 505 } 506 507 ToolInvocationLogger.logLaunchMessage(logDetails, providedArgList, 508 propertiesFileArgList, propertiesFilePath); 509 } 510 511 try 512 { 513 exitCode.set(doToolProcessing()); 514 } 515 catch (final Exception e) 516 { 517 Debug.debugException(e); 518 err(StaticUtils.getExceptionMessage(e)); 519 exitCode.set(ResultCode.LOCAL_ERROR); 520 } 521 finally 522 { 523 if (logShutdownHook != null) 524 { 525 Runtime.getRuntime().removeShutdownHook(logShutdownHook); 526 527 String completionMessage = getToolCompletionMessage(); 528 if (completionMessage == null) 529 { 530 completionMessage = exitCode.get().getName(); 531 } 532 533 ToolInvocationLogger.logCompletionMessage( 534 logDetails, exitCode.get().intValue(), completionMessage); 535 } 536 if (shutdownHook != null) 537 { 538 Runtime.getRuntime().removeShutdownHook(shutdownHook); 539 } 540 } 541 542 return exitCode.get(); 543 } 544 545 546 547 /** 548 * Updates the provided argument list with object pairs that comprise the 549 * set of arguments actually provided to this tool on the command line. 550 * 551 * @param parser The argument parser for this tool. 552 * It must not be {@code null}. 553 * @param argumentsSetFromPropertiesFile A set that includes all arguments 554 * set from the properties file. 555 * @param argList The list to which the argument 556 * information should be added. It 557 * must not be {@code null}. The 558 * first element of each object pair 559 * that is added must be 560 * non-{@code null}. The second 561 * element in any given pair may be 562 * {@code null} if the first element 563 * represents the name of an argument 564 * that doesn't take any values, the 565 * name of the selected subcommand, or 566 * an unnamed trailing argument. 567 */ 568 private static void getToolInvocationProvidedArguments( 569 final ArgumentParser parser, 570 final Set<Argument> argumentsSetFromPropertiesFile, 571 final List<ObjectPair<String,String>> argList) 572 { 573 final String noValue = null; 574 final SubCommand subCommand = parser.getSelectedSubCommand(); 575 if (subCommand != null) 576 { 577 argList.add(new ObjectPair<>(subCommand.getPrimaryName(), noValue)); 578 } 579 580 for (final Argument arg : parser.getNamedArguments()) 581 { 582 // Exclude arguments that weren't provided. 583 if (! arg.isPresent()) 584 { 585 continue; 586 } 587 588 // Exclude arguments that were set from the properties file. 589 if (argumentsSetFromPropertiesFile.contains(arg)) 590 { 591 continue; 592 } 593 594 if (arg.takesValue()) 595 { 596 for (final String value : arg.getValueStringRepresentations(false)) 597 { 598 if (arg.isSensitive()) 599 { 600 argList.add(new ObjectPair<>(arg.getIdentifierString(), 601 "*****REDACTED*****")); 602 } 603 else 604 { 605 argList.add(new ObjectPair<>(arg.getIdentifierString(), value)); 606 } 607 } 608 } 609 else 610 { 611 argList.add(new ObjectPair<>(arg.getIdentifierString(), noValue)); 612 } 613 } 614 615 if (subCommand != null) 616 { 617 getToolInvocationProvidedArguments(subCommand.getArgumentParser(), 618 argumentsSetFromPropertiesFile, argList); 619 } 620 621 for (final String trailingArgument : parser.getTrailingArguments()) 622 { 623 argList.add(new ObjectPair<>(trailingArgument, noValue)); 624 } 625 } 626 627 628 629 /** 630 * Updates the provided argument list with object pairs that comprise the 631 * set of tool arguments set from a properties file. 632 * 633 * @param parser The argument parser for this tool. 634 * It must not be {@code null}. 635 * @param argumentsSetFromPropertiesFile A set that should be updated with 636 * each argument set from the 637 * properties file. 638 * @param argList The list to which the argument 639 * information should be added. It 640 * must not be {@code null}. The 641 * first element of each object pair 642 * that is added must be 643 * non-{@code null}. The second 644 * element in any given pair may be 645 * {@code null} if the first element 646 * represents the name of an argument 647 * that doesn't take any values, the 648 * name of the selected subcommand, or 649 * an unnamed trailing argument. 650 */ 651 private static void getToolInvocationPropertiesFileArguments( 652 final ArgumentParser parser, 653 final Set<Argument> argumentsSetFromPropertiesFile, 654 final List<ObjectPair<String,String>> argList) 655 { 656 final ArgumentParser subCommandParser; 657 final SubCommand subCommand = parser.getSelectedSubCommand(); 658 if (subCommand == null) 659 { 660 subCommandParser = null; 661 } 662 else 663 { 664 subCommandParser = subCommand.getArgumentParser(); 665 } 666 667 final String noValue = null; 668 669 final Iterator<String> iterator = 670 parser.getArgumentsSetFromPropertiesFile().iterator(); 671 while (iterator.hasNext()) 672 { 673 final String arg = iterator.next(); 674 if (arg.startsWith("-")) 675 { 676 Argument a; 677 if (arg.startsWith("--")) 678 { 679 final String longIdentifier = arg.substring(2); 680 a = parser.getNamedArgument(longIdentifier); 681 if ((a == null) && (subCommandParser != null)) 682 { 683 a = subCommandParser.getNamedArgument(longIdentifier); 684 } 685 } 686 else 687 { 688 final char shortIdentifier = arg.charAt(1); 689 a = parser.getNamedArgument(shortIdentifier); 690 if ((a == null) && (subCommandParser != null)) 691 { 692 a = subCommandParser.getNamedArgument(shortIdentifier); 693 } 694 } 695 696 if (a != null) 697 { 698 argumentsSetFromPropertiesFile.add(a); 699 700 if (a.takesValue()) 701 { 702 final String value = iterator.next(); 703 if (a.isSensitive()) 704 { 705 argList.add(new ObjectPair<>(a.getIdentifierString(), noValue)); 706 } 707 else 708 { 709 argList.add(new ObjectPair<>(a.getIdentifierString(), value)); 710 } 711 } 712 else 713 { 714 argList.add(new ObjectPair<>(a.getIdentifierString(), noValue)); 715 } 716 } 717 } 718 else 719 { 720 argList.add(new ObjectPair<>(arg, noValue)); 721 } 722 } 723 } 724 725 726 727 /** 728 * Retrieves a sorted map of subcommands for the provided argument parser, 729 * alphabetized by primary name. 730 * 731 * @param parser The argument parser for which to get the sorted 732 * subcommands. 733 * 734 * @return The sorted map of subcommands. 735 */ 736 private static TreeMap<String,SubCommand> getSortedSubCommands( 737 final ArgumentParser parser) 738 { 739 final TreeMap<String,SubCommand> m = new TreeMap<>(); 740 for (final SubCommand sc : parser.getSubCommands()) 741 { 742 m.put(sc.getPrimaryName(), sc); 743 } 744 return m; 745 } 746 747 748 749 /** 750 * Writes example usage information for this tool to the standard output 751 * stream. 752 * 753 * @param parser The argument parser used to process the provided set of 754 * command-line arguments. 755 */ 756 private void displayExampleUsages(final ArgumentParser parser) 757 { 758 final LinkedHashMap<String[],String> examples; 759 if ((parser != null) && (parser.getSelectedSubCommand() != null)) 760 { 761 examples = parser.getSelectedSubCommand().getExampleUsages(); 762 } 763 else 764 { 765 examples = getExampleUsages(); 766 } 767 768 if ((examples == null) || examples.isEmpty()) 769 { 770 return; 771 } 772 773 out(INFO_CL_TOOL_LABEL_EXAMPLES); 774 775 final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 776 for (final Map.Entry<String[],String> e : examples.entrySet()) 777 { 778 out(); 779 wrapOut(2, wrapWidth, e.getValue()); 780 out(); 781 782 final StringBuilder buffer = new StringBuilder(); 783 buffer.append(" "); 784 buffer.append(getToolName()); 785 786 final String[] args = e.getKey(); 787 for (int i=0; i < args.length; i++) 788 { 789 buffer.append(' '); 790 791 // If the argument has a value, then make sure to keep it on the same 792 // line as the argument name. This may introduce false positives due to 793 // unnamed trailing arguments, but the worst that will happen that case 794 // is that the output may be wrapped earlier than necessary one time. 795 String arg = args[i]; 796 if (arg.startsWith("-")) 797 { 798 if ((i < (args.length - 1)) && (! args[i+1].startsWith("-"))) 799 { 800 final ExampleCommandLineArgument cleanArg = 801 ExampleCommandLineArgument.getCleanArgument(args[i+1]); 802 arg += ' ' + cleanArg.getLocalForm(); 803 i++; 804 } 805 } 806 else 807 { 808 final ExampleCommandLineArgument cleanArg = 809 ExampleCommandLineArgument.getCleanArgument(arg); 810 arg = cleanArg.getLocalForm(); 811 } 812 813 if ((buffer.length() + arg.length() + 2) < wrapWidth) 814 { 815 buffer.append(arg); 816 } 817 else 818 { 819 buffer.append('\\'); 820 out(buffer.toString()); 821 buffer.setLength(0); 822 buffer.append(" "); 823 buffer.append(arg); 824 } 825 } 826 827 out(buffer.toString()); 828 } 829 } 830 831 832 833 /** 834 * Retrieves the name of this tool. It should be the name of the command used 835 * to invoke this tool. 836 * 837 * @return The name for this tool. 838 */ 839 public abstract String getToolName(); 840 841 842 843 /** 844 * Retrieves a human-readable description for this tool. If the description 845 * should include multiple paragraphs, then this method should return the text 846 * for the first paragraph, and the 847 * {@link #getAdditionalDescriptionParagraphs()} method should be used to 848 * return the text for the subsequent paragraphs. 849 * 850 * @return A human-readable description for this tool. 851 */ 852 public abstract String getToolDescription(); 853 854 855 856 /** 857 * Retrieves additional paragraphs that should be included in the description 858 * for this tool. If the tool description should include multiple paragraphs, 859 * then the {@link #getToolDescription()} method should return the text of the 860 * first paragraph, and each item in the list returned by this method should 861 * be the text for each subsequent paragraph. If the tool description should 862 * only have a single paragraph, then this method may return {@code null} or 863 * an empty list. 864 * 865 * @return Additional paragraphs that should be included in the description 866 * for this tool, or {@code null} or an empty list if only a single 867 * description paragraph (whose text is returned by the 868 * {@code getToolDescription} method) is needed. 869 */ 870 public List<String> getAdditionalDescriptionParagraphs() 871 { 872 return Collections.emptyList(); 873 } 874 875 876 877 /** 878 * Retrieves a version string for this tool, if available. 879 * 880 * @return A version string for this tool, or {@code null} if none is 881 * available. 882 */ 883 public String getToolVersion() 884 { 885 return null; 886 } 887 888 889 890 /** 891 * Retrieves the minimum number of unnamed trailing arguments that must be 892 * provided for this tool. If a tool requires the use of trailing arguments, 893 * then it must override this method and the {@link #getMaxTrailingArguments} 894 * arguments to return nonzero values, and it must also override the 895 * {@link #getTrailingArgumentsPlaceholder} method to return a 896 * non-{@code null} value. 897 * 898 * @return The minimum number of unnamed trailing arguments that may be 899 * provided for this tool. A value of zero indicates that the tool 900 * may be invoked without any trailing arguments. 901 */ 902 public int getMinTrailingArguments() 903 { 904 return 0; 905 } 906 907 908 909 /** 910 * Retrieves the maximum number of unnamed trailing arguments that may be 911 * provided for this tool. If a tool supports trailing arguments, then it 912 * must override this method to return a nonzero value, and must also override 913 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to 914 * return a non-{@code null} value. 915 * 916 * @return The maximum number of unnamed trailing arguments that may be 917 * provided for this tool. A value of zero indicates that trailing 918 * arguments are not allowed. A negative value indicates that there 919 * should be no limit on the number of trailing arguments. 920 */ 921 public int getMaxTrailingArguments() 922 { 923 return 0; 924 } 925 926 927 928 /** 929 * Retrieves a placeholder string that should be used for trailing arguments 930 * in the usage information for this tool. 931 * 932 * @return A placeholder string that should be used for trailing arguments in 933 * the usage information for this tool, or {@code null} if trailing 934 * arguments are not supported. 935 */ 936 public String getTrailingArgumentsPlaceholder() 937 { 938 return null; 939 } 940 941 942 943 /** 944 * Indicates whether this tool should provide support for an interactive mode, 945 * in which the tool offers a mode in which the arguments can be provided in 946 * a text-driven menu rather than requiring them to be given on the command 947 * line. If interactive mode is supported, it may be invoked using the 948 * "--interactive" argument. Alternately, if interactive mode is supported 949 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 950 * interactive mode may be invoked by simply launching the tool without any 951 * arguments. 952 * 953 * @return {@code true} if this tool supports interactive mode, or 954 * {@code false} if not. 955 */ 956 public boolean supportsInteractiveMode() 957 { 958 return false; 959 } 960 961 962 963 /** 964 * Indicates whether this tool defaults to launching in interactive mode if 965 * the tool is invoked without any command-line arguments. This will only be 966 * used if {@link #supportsInteractiveMode()} returns {@code true}. 967 * 968 * @return {@code true} if this tool defaults to using interactive mode if 969 * launched without any command-line arguments, or {@code false} if 970 * not. 971 */ 972 public boolean defaultsToInteractiveMode() 973 { 974 return false; 975 } 976 977 978 979 /** 980 * Interactively prompts the user for information needed to invoke this tool 981 * and returns an appropriate list of arguments that should be used to run it. 982 * <BR><BR> 983 * This method will only be invoked if {@link #supportsInteractiveMode()} 984 * returns {@code true}, and if one of the following conditions is satisfied: 985 * <UL> 986 * <LI>The {@code --interactive} argument is explicitly provided on the 987 * command line.</LI> 988 * <LI>The tool was invoked without any command-line arguments and 989 * {@link #defaultsToInteractiveMode()} returns {@code true}.</LI> 990 * </UL> 991 * If this method is invoked and returns {@code null}, then the LDAP SDK's 992 * default interactive mode processing will be performed. Otherwise, the tool 993 * will be invoked with only the arguments in the list that is returned. 994 * 995 * @param parser The argument parser that has been used to parse any 996 * command-line arguments that were provided before the 997 * interactive mode processing was invoked. If this method 998 * returns a non-{@code null} value, then this parser will be 999 * reset before parsing the new set of arguments. 1000 * 1001 * @return Retrieves a list of command-line arguments that may be used to 1002 * invoke this tool, or {@code null} if the LDAP SDK's default 1003 * interactive mode processing should be performed. 1004 * 1005 * @throws LDAPException If a problem is encountered while interactively 1006 * obtaining the arguments that should be used to 1007 * run the tool. 1008 */ 1009 protected List<String> requestToolArgumentsInteractively( 1010 final ArgumentParser parser) 1011 throws LDAPException 1012 { 1013 // Fall back to using the LDAP SDK's default interactive mode processor. 1014 return null; 1015 } 1016 1017 1018 1019 /** 1020 * Indicates whether this tool supports the use of a properties file for 1021 * specifying default values for arguments that aren't specified on the 1022 * command line. 1023 * 1024 * @return {@code true} if this tool supports the use of a properties file 1025 * for specifying default values for arguments that aren't specified 1026 * on the command line, or {@code false} if not. 1027 */ 1028 public boolean supportsPropertiesFile() 1029 { 1030 return false; 1031 } 1032 1033 1034 1035 /** 1036 * Indicates whether this tool should provide arguments for redirecting output 1037 * to a file. If this method returns {@code true}, then the tool will offer 1038 * an "--outputFile" argument that will specify the path to a file to which 1039 * all standard output and standard error content will be written, and it will 1040 * also offer a "--teeToStandardOut" argument that can only be used if the 1041 * "--outputFile" argument is present and will cause all output to be written 1042 * to both the specified output file and to standard output. 1043 * 1044 * @return {@code true} if this tool should provide arguments for redirecting 1045 * output to a file, or {@code false} if not. 1046 */ 1047 protected boolean supportsOutputFile() 1048 { 1049 return false; 1050 } 1051 1052 1053 1054 /** 1055 * Indicates whether to log messages about the launch and completion of this 1056 * tool into the invocation log of Ping Identity server products that may 1057 * include it. This method is not needed for tools that are not expected to 1058 * be part of the Ping Identity server products suite. Further, this value 1059 * may be overridden by settings in the server's 1060 * tool-invocation-logging.properties file. 1061 * <BR><BR> 1062 * This method should generally return {@code true} for tools that may alter 1063 * the server configuration, data, or other state information, and 1064 * {@code false} for tools that do not make any changes. 1065 * 1066 * @return {@code true} if Ping Identity server products should include 1067 * messages about the launch and completion of this tool in tool 1068 * invocation log files by default, or {@code false} if not. 1069 */ 1070 protected boolean logToolInvocationByDefault() 1071 { 1072 return false; 1073 } 1074 1075 1076 1077 /** 1078 * Retrieves an optional message that may provide additional information about 1079 * the way that the tool completed its processing. For example if the tool 1080 * exited with an error message, it may be useful for this method to return 1081 * that error message. 1082 * <BR><BR> 1083 * The message returned by this method is intended for purposes and is not 1084 * meant to be parsed or programmatically interpreted. 1085 * 1086 * @return An optional message that may provide additional information about 1087 * the completion state for this tool, or {@code null} if no 1088 * completion message is available. 1089 */ 1090 protected String getToolCompletionMessage() 1091 { 1092 return null; 1093 } 1094 1095 1096 1097 /** 1098 * Creates a parser that can be used to to parse arguments accepted by 1099 * this tool. 1100 * 1101 * @return ArgumentParser that can be used to parse arguments for this 1102 * tool. 1103 * 1104 * @throws ArgumentException If there was a problem initializing the 1105 * parser for this tool. 1106 */ 1107 public final ArgumentParser createArgumentParser() 1108 throws ArgumentException 1109 { 1110 final ArgumentParser parser = new ArgumentParser(getToolName(), 1111 getToolDescription(), getAdditionalDescriptionParagraphs(), 1112 getMinTrailingArguments(), getMaxTrailingArguments(), 1113 getTrailingArgumentsPlaceholder()); 1114 parser.setCommandLineTool(this); 1115 1116 addToolArguments(parser); 1117 1118 if (supportsInteractiveMode()) 1119 { 1120 interactiveArgument = new BooleanArgument(null, "interactive", 1121 INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get()); 1122 interactiveArgument.setUsageArgument(true); 1123 parser.addArgument(interactiveArgument); 1124 } 1125 1126 if (supportsOutputFile()) 1127 { 1128 outputFileArgument = new FileArgument(null, "outputFile", false, 1, null, 1129 INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true, 1130 false); 1131 outputFileArgument.addLongIdentifier("output-file", true); 1132 outputFileArgument.setUsageArgument(true); 1133 parser.addArgument(outputFileArgument); 1134 1135 appendToOutputFileArgument = new BooleanArgument(null, 1136 "appendToOutputFile", 1, 1137 INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get( 1138 outputFileArgument.getIdentifierString())); 1139 appendToOutputFileArgument.addLongIdentifier("append-to-output-file", 1140 true); 1141 appendToOutputFileArgument.setUsageArgument(true); 1142 parser.addArgument(appendToOutputFileArgument); 1143 1144 teeOutputArgument = new BooleanArgument(null, "teeOutput", 1, 1145 INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get( 1146 outputFileArgument.getIdentifierString())); 1147 teeOutputArgument.addLongIdentifier("tee-output", true); 1148 teeOutputArgument.setUsageArgument(true); 1149 parser.addArgument(teeOutputArgument); 1150 1151 parser.addDependentArgumentSet(appendToOutputFileArgument, 1152 outputFileArgument); 1153 parser.addDependentArgumentSet(teeOutputArgument, 1154 outputFileArgument); 1155 } 1156 1157 helpArgument = new BooleanArgument('H', "help", 1158 INFO_CL_TOOL_DESCRIPTION_HELP.get()); 1159 helpArgument.addShortIdentifier('?', true); 1160 helpArgument.setUsageArgument(true); 1161 parser.addArgument(helpArgument); 1162 1163 if (! parser.getSubCommands().isEmpty()) 1164 { 1165 helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1, 1166 INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get()); 1167 helpSubcommandsArgument.addLongIdentifier("helpSubcommand", true); 1168 helpSubcommandsArgument.addLongIdentifier("help-subcommands", true); 1169 helpSubcommandsArgument.addLongIdentifier("help-subcommand", true); 1170 helpSubcommandsArgument.setUsageArgument(true); 1171 parser.addArgument(helpSubcommandsArgument); 1172 } 1173 1174 final String version = getToolVersion(); 1175 if ((version != null) && (! version.isEmpty()) && 1176 (parser.getNamedArgument("version") == null)) 1177 { 1178 final Character shortIdentifier; 1179 if (parser.getNamedArgument('V') == null) 1180 { 1181 shortIdentifier = 'V'; 1182 } 1183 else 1184 { 1185 shortIdentifier = null; 1186 } 1187 1188 versionArgument = new BooleanArgument(shortIdentifier, "version", 1189 INFO_CL_TOOL_DESCRIPTION_VERSION.get()); 1190 versionArgument.setUsageArgument(true); 1191 parser.addArgument(versionArgument); 1192 } 1193 1194 if (supportsPropertiesFile()) 1195 { 1196 parser.enablePropertiesFileSupport(); 1197 } 1198 1199 return parser; 1200 } 1201 1202 1203 1204 /** 1205 * Specifies the argument that is used to retrieve usage information about 1206 * SASL authentication. 1207 * 1208 * @param helpSASLArgument The argument that is used to retrieve usage 1209 * information about SASL authentication. 1210 */ 1211 void setHelpSASLArgument(final BooleanArgument helpSASLArgument) 1212 { 1213 this.helpSASLArgument = helpSASLArgument; 1214 } 1215 1216 1217 1218 /** 1219 * Adds the provided argument to the set of arguments that may be used to 1220 * enable JVM SSL/TLS debugging. 1221 * 1222 * @param enableSSLDebuggingArgument The argument to add to the set of 1223 * arguments that may be used to enable 1224 * JVM SSL/TLS debugging. 1225 */ 1226 protected void addEnableSSLDebuggingArgument( 1227 final BooleanArgument enableSSLDebuggingArgument) 1228 { 1229 enableSSLDebuggingArguments.add(enableSSLDebuggingArgument); 1230 } 1231 1232 1233 1234 /** 1235 * Retrieves a set containing the long identifiers used for usage arguments 1236 * injected by this class. 1237 * 1238 * @param tool The tool to use to help make the determination. 1239 * 1240 * @return A set containing the long identifiers used for usage arguments 1241 * injected by this class. 1242 */ 1243 static Set<String> getUsageArgumentIdentifiers(final CommandLineTool tool) 1244 { 1245 final LinkedHashSet<String> ids = 1246 new LinkedHashSet<>(StaticUtils.computeMapCapacity(9)); 1247 1248 ids.add("help"); 1249 ids.add("version"); 1250 ids.add("helpSubcommands"); 1251 1252 if (tool.supportsInteractiveMode()) 1253 { 1254 ids.add("interactive"); 1255 } 1256 1257 if (tool.supportsPropertiesFile()) 1258 { 1259 ids.add("propertiesFilePath"); 1260 ids.add("generatePropertiesFile"); 1261 ids.add("noPropertiesFile"); 1262 ids.add("suppressPropertiesFileComment"); 1263 } 1264 1265 if (tool.supportsOutputFile()) 1266 { 1267 ids.add("outputFile"); 1268 ids.add("appendToOutputFile"); 1269 ids.add("teeOutput"); 1270 } 1271 1272 return Collections.unmodifiableSet(ids); 1273 } 1274 1275 1276 1277 /** 1278 * Adds the command-line arguments supported for use with this tool to the 1279 * provided argument parser. The tool may need to retain references to the 1280 * arguments (and/or the argument parser, if trailing arguments are allowed) 1281 * to it in order to obtain their values for use in later processing. 1282 * 1283 * @param parser The argument parser to which the arguments are to be added. 1284 * 1285 * @throws ArgumentException If a problem occurs while adding any of the 1286 * tool-specific arguments to the provided 1287 * argument parser. 1288 */ 1289 public abstract void addToolArguments(ArgumentParser parser) 1290 throws ArgumentException; 1291 1292 1293 1294 /** 1295 * Performs any necessary processing that should be done to ensure that the 1296 * provided set of command-line arguments were valid. This method will be 1297 * called after the basic argument parsing has been performed and immediately 1298 * before the {@link CommandLineTool#doToolProcessing} method is invoked. 1299 * Note that if the tool supports interactive mode, then this method may be 1300 * invoked multiple times to allow the user to interactively fix validation 1301 * errors. 1302 * 1303 * @throws ArgumentException If there was a problem with the command-line 1304 * arguments provided to this program. 1305 */ 1306 public void doExtendedArgumentValidation() 1307 throws ArgumentException 1308 { 1309 // No processing will be performed by default. 1310 } 1311 1312 1313 1314 /** 1315 * Performs the core set of processing for this tool. 1316 * 1317 * @return A result code that indicates whether the processing completed 1318 * successfully. 1319 */ 1320 public abstract ResultCode doToolProcessing(); 1321 1322 1323 1324 /** 1325 * Indicates whether this tool should register a shutdown hook with the JVM. 1326 * Shutdown hooks allow for a best-effort attempt to perform a specified set 1327 * of processing when the JVM is shutting down under various conditions, 1328 * including: 1329 * <UL> 1330 * <LI>When all non-daemon threads have stopped running (i.e., the tool has 1331 * completed processing).</LI> 1332 * <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI> 1333 * <LI>When the JVM receives an external kill signal (e.g., via the use of 1334 * the kill tool or interrupting the JVM with Ctrl+C).</LI> 1335 * </UL> 1336 * Shutdown hooks may not be invoked if the process is forcefully killed 1337 * (e.g., using "kill -9", or the {@code System.halt()} or 1338 * {@code Runtime.halt()} methods). 1339 * <BR><BR> 1340 * If this method is overridden to return {@code true}, then the 1341 * {@link #doShutdownHookProcessing(ResultCode)} method should also be 1342 * overridden to contain the logic that will be invoked when the JVM is 1343 * shutting down in a manner that calls shutdown hooks. 1344 * 1345 * @return {@code true} if this tool should register a shutdown hook, or 1346 * {@code false} if not. 1347 */ 1348 protected boolean registerShutdownHook() 1349 { 1350 return false; 1351 } 1352 1353 1354 1355 /** 1356 * Performs any processing that may be needed when the JVM is shutting down, 1357 * whether because tool processing has completed or because it has been 1358 * interrupted (e.g., by a kill or break signal). 1359 * <BR><BR> 1360 * Note that because shutdown hooks run at a delicate time in the life of the 1361 * JVM, they should complete quickly and minimize access to external 1362 * resources. See the documentation for the 1363 * {@code java.lang.Runtime.addShutdownHook} method for recommendations and 1364 * restrictions about writing shutdown hooks. 1365 * 1366 * @param resultCode The result code returned by the tool. It may be 1367 * {@code null} if the tool was interrupted before it 1368 * completed processing. 1369 */ 1370 protected void doShutdownHookProcessing(final ResultCode resultCode) 1371 { 1372 throw new LDAPSDKUsageException( 1373 ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get( 1374 getToolName())); 1375 } 1376 1377 1378 1379 /** 1380 * Retrieves a set of information that may be used to generate example usage 1381 * information. Each element in the returned map should consist of a map 1382 * between an example set of arguments and a string that describes the 1383 * behavior of the tool when invoked with that set of arguments. 1384 * 1385 * @return A set of information that may be used to generate example usage 1386 * information. It may be {@code null} or empty if no example usage 1387 * information is available. 1388 */ 1389 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1390 public LinkedHashMap<String[],String> getExampleUsages() 1391 { 1392 return null; 1393 } 1394 1395 1396 1397 /** 1398 * Retrieves the password file reader for this tool, which may be used to 1399 * read passwords from (optionally compressed and encrypted) files. 1400 * 1401 * @return The password file reader for this tool. 1402 */ 1403 public final PasswordFileReader getPasswordFileReader() 1404 { 1405 return passwordFileReader; 1406 } 1407 1408 1409 1410 /** 1411 * Retrieves the print stream that will be used for standard output. 1412 * 1413 * @return The print stream that will be used for standard output. 1414 */ 1415 public final PrintStream getOut() 1416 { 1417 return out; 1418 } 1419 1420 1421 1422 /** 1423 * Retrieves the print stream that may be used to write to the original 1424 * standard output. This may be different from the current standard output 1425 * stream if an output file has been configured. 1426 * 1427 * @return The print stream that may be used to write to the original 1428 * standard output. 1429 */ 1430 public final PrintStream getOriginalOut() 1431 { 1432 return originalOut; 1433 } 1434 1435 1436 1437 /** 1438 * Writes the provided message to the standard output stream for this tool. 1439 * <BR><BR> 1440 * This method is completely threadsafe and my be invoked concurrently by any 1441 * number of threads. 1442 * 1443 * @param msg The message components that will be written to the standard 1444 * output stream. They will be concatenated together on the same 1445 * line, and that line will be followed by an end-of-line 1446 * sequence. 1447 */ 1448 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1449 public final synchronized void out(final Object... msg) 1450 { 1451 write(out, 0, 0, msg); 1452 } 1453 1454 1455 1456 /** 1457 * Writes the provided message to the standard output stream for this tool, 1458 * optionally wrapping and/or indenting the text in the process. 1459 * <BR><BR> 1460 * This method is completely threadsafe and my be invoked concurrently by any 1461 * number of threads. 1462 * 1463 * @param indent The number of spaces each line should be indented. A 1464 * value less than or equal to zero indicates that no 1465 * indent should be used. 1466 * @param wrapColumn The column at which to wrap long lines. A value less 1467 * than or equal to two indicates that no wrapping should 1468 * be performed. If both an indent and a wrap column are 1469 * to be used, then the wrap column must be greater than 1470 * the indent. 1471 * @param msg The message components that will be written to the 1472 * standard output stream. They will be concatenated 1473 * together on the same line, and that line will be 1474 * followed by an end-of-line sequence. 1475 */ 1476 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1477 public final synchronized void wrapOut(final int indent, final int wrapColumn, 1478 final Object... msg) 1479 { 1480 write(out, indent, wrapColumn, msg); 1481 } 1482 1483 1484 1485 /** 1486 * Writes the provided message to the standard output stream for this tool, 1487 * optionally wrapping and/or indenting the text in the process. 1488 * <BR><BR> 1489 * This method is completely threadsafe and my be invoked concurrently by any 1490 * number of threads. 1491 * 1492 * @param firstLineIndent The number of spaces the first line should be 1493 * indented. A value less than or equal to zero 1494 * indicates that no indent should be used. 1495 * @param subsequentLineIndent The number of spaces each line except the 1496 * first should be indented. A value less than 1497 * or equal to zero indicates that no indent 1498 * should be used. 1499 * @param wrapColumn The column at which to wrap long lines. A 1500 * value less than or equal to two indicates 1501 * that no wrapping should be performed. If 1502 * both an indent and a wrap column are to be 1503 * used, then the wrap column must be greater 1504 * than the indent. 1505 * @param endWithNewline Indicates whether a newline sequence should 1506 * follow the last line that is printed. 1507 * @param msg The message components that will be written 1508 * to the standard output stream. They will be 1509 * concatenated together on the same line, and 1510 * that line will be followed by an end-of-line 1511 * sequence. 1512 */ 1513 final synchronized void wrapStandardOut(final int firstLineIndent, 1514 final int subsequentLineIndent, 1515 final int wrapColumn, 1516 final boolean endWithNewline, 1517 final Object... msg) 1518 { 1519 write(out, firstLineIndent, subsequentLineIndent, wrapColumn, 1520 endWithNewline, msg); 1521 } 1522 1523 1524 1525 /** 1526 * Retrieves the print stream that will be used for standard error. 1527 * 1528 * @return The print stream that will be used for standard error. 1529 */ 1530 public final PrintStream getErr() 1531 { 1532 return err; 1533 } 1534 1535 1536 1537 /** 1538 * Retrieves the print stream that may be used to write to the original 1539 * standard error. This may be different from the current standard error 1540 * stream if an output file has been configured. 1541 * 1542 * @return The print stream that may be used to write to the original 1543 * standard error. 1544 */ 1545 public final PrintStream getOriginalErr() 1546 { 1547 return originalErr; 1548 } 1549 1550 1551 1552 /** 1553 * Writes the provided message to the standard error stream for this tool. 1554 * <BR><BR> 1555 * This method is completely threadsafe and my be invoked concurrently by any 1556 * number of threads. 1557 * 1558 * @param msg The message components that will be written to the standard 1559 * error stream. They will be concatenated together on the same 1560 * line, and that line will be followed by an end-of-line 1561 * sequence. 1562 */ 1563 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1564 public final synchronized void err(final Object... msg) 1565 { 1566 write(err, 0, 0, msg); 1567 } 1568 1569 1570 1571 /** 1572 * Writes the provided message to the standard error stream for this tool, 1573 * optionally wrapping and/or indenting the text in the process. 1574 * <BR><BR> 1575 * This method is completely threadsafe and my be invoked concurrently by any 1576 * number of threads. 1577 * 1578 * @param indent The number of spaces each line should be indented. A 1579 * value less than or equal to zero indicates that no 1580 * indent should be used. 1581 * @param wrapColumn The column at which to wrap long lines. A value less 1582 * than or equal to two indicates that no wrapping should 1583 * be performed. If both an indent and a wrap column are 1584 * to be used, then the wrap column must be greater than 1585 * the indent. 1586 * @param msg The message components that will be written to the 1587 * standard output stream. They will be concatenated 1588 * together on the same line, and that line will be 1589 * followed by an end-of-line sequence. 1590 */ 1591 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1592 public final synchronized void wrapErr(final int indent, final int wrapColumn, 1593 final Object... msg) 1594 { 1595 write(err, indent, wrapColumn, msg); 1596 } 1597 1598 1599 1600 /** 1601 * Writes the provided message to the given print stream, optionally wrapping 1602 * and/or indenting the text in the process. 1603 * 1604 * @param stream The stream to which the message should be written. 1605 * @param indent The number of spaces each line should be indented. A 1606 * value less than or equal to zero indicates that no 1607 * indent should be used. 1608 * @param wrapColumn The column at which to wrap long lines. A value less 1609 * than or equal to two indicates that no wrapping should 1610 * be performed. If both an indent and a wrap column are 1611 * to be used, then the wrap column must be greater than 1612 * the indent. 1613 * @param msg The message components that will be written to the 1614 * standard output stream. They will be concatenated 1615 * together on the same line, and that line will be 1616 * followed by an end-of-line sequence. 1617 */ 1618 private static void write(final PrintStream stream, final int indent, 1619 final int wrapColumn, final Object... msg) 1620 { 1621 write(stream, indent, indent, wrapColumn, true, msg); 1622 } 1623 1624 1625 1626 /** 1627 * Writes the provided message to the given print stream, optionally wrapping 1628 * and/or indenting the text in the process. 1629 * 1630 * @param stream The stream to which the message should be 1631 * written. 1632 * @param firstLineIndent The number of spaces the first line should be 1633 * indented. A value less than or equal to zero 1634 * indicates that no indent should be used. 1635 * @param subsequentLineIndent The number of spaces all lines after the 1636 * first should be indented. A value less than 1637 * or equal to zero indicates that no indent 1638 * should be used. 1639 * @param wrapColumn The column at which to wrap long lines. A 1640 * value less than or equal to two indicates 1641 * that no wrapping should be performed. If 1642 * both an indent and a wrap column are to be 1643 * used, then the wrap column must be greater 1644 * than the indent. 1645 * @param endWithNewline Indicates whether a newline sequence should 1646 * follow the last line that is printed. 1647 * @param msg The message components that will be written 1648 * to the standard output stream. They will be 1649 * concatenated together on the same line, and 1650 * that line will be followed by an end-of-line 1651 * sequence. 1652 */ 1653 private static void write(final PrintStream stream, final int firstLineIndent, 1654 final int subsequentLineIndent, 1655 final int wrapColumn, 1656 final boolean endWithNewline, final Object... msg) 1657 { 1658 final StringBuilder buffer = new StringBuilder(); 1659 for (final Object o : msg) 1660 { 1661 buffer.append(o); 1662 } 1663 1664 if (wrapColumn > 2) 1665 { 1666 boolean firstLine = true; 1667 for (final String line : 1668 StaticUtils.wrapLine(buffer.toString(), 1669 (wrapColumn - firstLineIndent), 1670 (wrapColumn - subsequentLineIndent))) 1671 { 1672 final int indent; 1673 if (firstLine) 1674 { 1675 indent = firstLineIndent; 1676 firstLine = false; 1677 } 1678 else 1679 { 1680 stream.println(); 1681 indent = subsequentLineIndent; 1682 } 1683 1684 if (indent > 0) 1685 { 1686 for (int i=0; i < indent; i++) 1687 { 1688 stream.print(' '); 1689 } 1690 } 1691 stream.print(line); 1692 } 1693 } 1694 else 1695 { 1696 if (firstLineIndent > 0) 1697 { 1698 for (int i=0; i < firstLineIndent; i++) 1699 { 1700 stream.print(' '); 1701 } 1702 } 1703 stream.print(buffer.toString()); 1704 } 1705 1706 if (endWithNewline) 1707 { 1708 stream.println(); 1709 } 1710 stream.flush(); 1711 } 1712}