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