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