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