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