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