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