001/* 002 * Copyright 2008-2017 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 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().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"); 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 appendToOutputFileArgument.setUsageArgument(true); 1045 parser.addArgument(appendToOutputFileArgument); 1046 1047 teeOutputArgument = new BooleanArgument(null, "teeOutput", 1, 1048 INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get( 1049 outputFileArgument.getIdentifierString())); 1050 teeOutputArgument.addLongIdentifier("tee-output"); 1051 teeOutputArgument.setUsageArgument(true); 1052 parser.addArgument(teeOutputArgument); 1053 1054 parser.addDependentArgumentSet(appendToOutputFileArgument, 1055 outputFileArgument); 1056 parser.addDependentArgumentSet(teeOutputArgument, 1057 outputFileArgument); 1058 } 1059 1060 helpArgument = new BooleanArgument('H', "help", 1061 INFO_CL_TOOL_DESCRIPTION_HELP.get()); 1062 helpArgument.addShortIdentifier('?'); 1063 helpArgument.setUsageArgument(true); 1064 parser.addArgument(helpArgument); 1065 1066 if (! parser.getSubCommands().isEmpty()) 1067 { 1068 helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1, 1069 INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get()); 1070 helpSubcommandsArgument.addLongIdentifier("help-subcommands"); 1071 helpSubcommandsArgument.setUsageArgument(true); 1072 parser.addArgument(helpSubcommandsArgument); 1073 } 1074 1075 final String version = getToolVersion(); 1076 if ((version != null) && (version.length() > 0) && 1077 (parser.getNamedArgument("version") == null)) 1078 { 1079 final Character shortIdentifier; 1080 if (parser.getNamedArgument('V') == null) 1081 { 1082 shortIdentifier = 'V'; 1083 } 1084 else 1085 { 1086 shortIdentifier = null; 1087 } 1088 1089 versionArgument = new BooleanArgument(shortIdentifier, "version", 1090 INFO_CL_TOOL_DESCRIPTION_VERSION.get()); 1091 versionArgument.setUsageArgument(true); 1092 parser.addArgument(versionArgument); 1093 } 1094 1095 if (supportsPropertiesFile()) 1096 { 1097 parser.enablePropertiesFileSupport(); 1098 } 1099 1100 return parser; 1101 } 1102 1103 1104 1105 /** 1106 * Specifies the argument that is used to retrieve usage information about 1107 * SASL authentication. 1108 * 1109 * @param helpSASLArgument The argument that is used to retrieve usage 1110 * information about SASL authentication. 1111 */ 1112 void setHelpSASLArgument(final BooleanArgument helpSASLArgument) 1113 { 1114 this.helpSASLArgument = helpSASLArgument; 1115 } 1116 1117 1118 1119 /** 1120 * Retrieves a set containing the long identifiers used for usage arguments 1121 * injected by this class. 1122 * 1123 * @param tool The tool to use to help make the determination. 1124 * 1125 * @return A set containing the long identifiers used for usage arguments 1126 * injected by this class. 1127 */ 1128 static Set<String> getUsageArgumentIdentifiers(final CommandLineTool tool) 1129 { 1130 final LinkedHashSet<String> ids = new LinkedHashSet<String>(9); 1131 1132 ids.add("help"); 1133 ids.add("version"); 1134 ids.add("helpSubcommands"); 1135 1136 if (tool.supportsInteractiveMode()) 1137 { 1138 ids.add("interactive"); 1139 } 1140 1141 if (tool.supportsPropertiesFile()) 1142 { 1143 ids.add("propertiesFilePath"); 1144 ids.add("generatePropertiesFile"); 1145 ids.add("noPropertiesFile"); 1146 ids.add("suppressPropertiesFileComment"); 1147 } 1148 1149 if (tool.supportsOutputFile()) 1150 { 1151 ids.add("outputFile"); 1152 ids.add("appendToOutputFile"); 1153 ids.add("teeOutput"); 1154 } 1155 1156 return Collections.unmodifiableSet(ids); 1157 } 1158 1159 1160 1161 /** 1162 * Adds the command-line arguments supported for use with this tool to the 1163 * provided argument parser. The tool may need to retain references to the 1164 * arguments (and/or the argument parser, if trailing arguments are allowed) 1165 * to it in order to obtain their values for use in later processing. 1166 * 1167 * @param parser The argument parser to which the arguments are to be added. 1168 * 1169 * @throws ArgumentException If a problem occurs while adding any of the 1170 * tool-specific arguments to the provided 1171 * argument parser. 1172 */ 1173 public abstract void addToolArguments(ArgumentParser parser) 1174 throws ArgumentException; 1175 1176 1177 1178 /** 1179 * Performs any necessary processing that should be done to ensure that the 1180 * provided set of command-line arguments were valid. This method will be 1181 * called after the basic argument parsing has been performed and immediately 1182 * before the {@link CommandLineTool#doToolProcessing} method is invoked. 1183 * Note that if the tool supports interactive mode, then this method may be 1184 * invoked multiple times to allow the user to interactively fix validation 1185 * errors. 1186 * 1187 * @throws ArgumentException If there was a problem with the command-line 1188 * arguments provided to this program. 1189 */ 1190 public void doExtendedArgumentValidation() 1191 throws ArgumentException 1192 { 1193 // No processing will be performed by default. 1194 } 1195 1196 1197 1198 /** 1199 * Performs the core set of processing for this tool. 1200 * 1201 * @return A result code that indicates whether the processing completed 1202 * successfully. 1203 */ 1204 public abstract ResultCode doToolProcessing(); 1205 1206 1207 1208 /** 1209 * Indicates whether this tool should register a shutdown hook with the JVM. 1210 * Shutdown hooks allow for a best-effort attempt to perform a specified set 1211 * of processing when the JVM is shutting down under various conditions, 1212 * including: 1213 * <UL> 1214 * <LI>When all non-daemon threads have stopped running (i.e., the tool has 1215 * completed processing).</LI> 1216 * <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI> 1217 * <LI>When the JVM receives an external kill signal (e.g., via the use of 1218 * the kill tool or interrupting the JVM with Ctrl+C).</LI> 1219 * </UL> 1220 * Shutdown hooks may not be invoked if the process is forcefully killed 1221 * (e.g., using "kill -9", or the {@code System.halt()} or 1222 * {@code Runtime.halt()} methods). 1223 * <BR><BR> 1224 * If this method is overridden to return {@code true}, then the 1225 * {@link #doShutdownHookProcessing(ResultCode)} method should also be 1226 * overridden to contain the logic that will be invoked when the JVM is 1227 * shutting down in a manner that calls shutdown hooks. 1228 * 1229 * @return {@code true} if this tool should register a shutdown hook, or 1230 * {@code false} if not. 1231 */ 1232 protected boolean registerShutdownHook() 1233 { 1234 return false; 1235 } 1236 1237 1238 1239 /** 1240 * Performs any processing that may be needed when the JVM is shutting down, 1241 * whether because tool processing has completed or because it has been 1242 * interrupted (e.g., by a kill or break signal). 1243 * <BR><BR> 1244 * Note that because shutdown hooks run at a delicate time in the life of the 1245 * JVM, they should complete quickly and minimize access to external 1246 * resources. See the documentation for the 1247 * {@code java.lang.Runtime.addShutdownHook} method for recommendations and 1248 * restrictions about writing shutdown hooks. 1249 * 1250 * @param resultCode The result code returned by the tool. It may be 1251 * {@code null} if the tool was interrupted before it 1252 * completed processing. 1253 */ 1254 protected void doShutdownHookProcessing(final ResultCode resultCode) 1255 { 1256 throw new LDAPSDKUsageException( 1257 ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get( 1258 getToolName())); 1259 } 1260 1261 1262 1263 /** 1264 * Retrieves a set of information that may be used to generate example usage 1265 * information. Each element in the returned map should consist of a map 1266 * between an example set of arguments and a string that describes the 1267 * behavior of the tool when invoked with that set of arguments. 1268 * 1269 * @return A set of information that may be used to generate example usage 1270 * information. It may be {@code null} or empty if no example usage 1271 * information is available. 1272 */ 1273 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1274 public LinkedHashMap<String[],String> getExampleUsages() 1275 { 1276 return null; 1277 } 1278 1279 1280 1281 /** 1282 * Retrieves the print stream that will be used for standard output. 1283 * 1284 * @return The print stream that will be used for standard output. 1285 */ 1286 public final PrintStream getOut() 1287 { 1288 return out; 1289 } 1290 1291 1292 1293 /** 1294 * Retrieves the print stream that may be used to write to the original 1295 * standard output. This may be different from the current standard output 1296 * stream if an output file has been configured. 1297 * 1298 * @return The print stream that may be used to write to the original 1299 * standard output. 1300 */ 1301 public final PrintStream getOriginalOut() 1302 { 1303 return originalOut; 1304 } 1305 1306 1307 1308 /** 1309 * Writes the provided message to the standard output stream for this tool. 1310 * <BR><BR> 1311 * This method is completely threadsafe and my be invoked concurrently by any 1312 * number of threads. 1313 * 1314 * @param msg The message components that will be written to the standard 1315 * output stream. They will be concatenated together on the same 1316 * line, and that line will be followed by an end-of-line 1317 * sequence. 1318 */ 1319 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1320 public final synchronized void out(final Object... msg) 1321 { 1322 write(out, 0, 0, msg); 1323 } 1324 1325 1326 1327 /** 1328 * Writes the provided message to the standard output stream for this tool, 1329 * optionally wrapping and/or indenting the text in the process. 1330 * <BR><BR> 1331 * This method is completely threadsafe and my be invoked concurrently by any 1332 * number of threads. 1333 * 1334 * @param indent The number of spaces each line should be indented. A 1335 * value less than or equal to zero indicates that no 1336 * indent should be used. 1337 * @param wrapColumn The column at which to wrap long lines. A value less 1338 * than or equal to two indicates that no wrapping should 1339 * be performed. If both an indent and a wrap column are 1340 * to be used, then the wrap column must be greater than 1341 * the indent. 1342 * @param msg The message components that will be written to the 1343 * standard output stream. They will be concatenated 1344 * together on the same line, and that line will be 1345 * followed by an end-of-line sequence. 1346 */ 1347 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1348 public final synchronized void wrapOut(final int indent, final int wrapColumn, 1349 final Object... msg) 1350 { 1351 write(out, indent, wrapColumn, msg); 1352 } 1353 1354 1355 1356 /** 1357 * Writes the provided message to the standard output stream for this tool, 1358 * optionally wrapping and/or indenting the text in the process. 1359 * <BR><BR> 1360 * This method is completely threadsafe and my be invoked concurrently by any 1361 * number of threads. 1362 * 1363 * @param firstLineIndent The number of spaces the first line should be 1364 * indented. A value less than or equal to zero 1365 * indicates that no indent should be used. 1366 * @param subsequentLineIndent The number of spaces each line except the 1367 * first should be indented. A value less than 1368 * or equal to zero indicates that no indent 1369 * should be used. 1370 * @param wrapColumn The column at which to wrap long lines. A 1371 * value less than or equal to two indicates 1372 * that no wrapping should be performed. If 1373 * both an indent and a wrap column are to be 1374 * used, then the wrap column must be greater 1375 * than the indent. 1376 * @param endWithNewline Indicates whether a newline sequence should 1377 * follow the last line that is printed. 1378 * @param msg The message components that will be written 1379 * to the standard output stream. They will be 1380 * concatenated together on the same line, and 1381 * that line will be followed by an end-of-line 1382 * sequence. 1383 */ 1384 final synchronized void wrapStandardOut(final int firstLineIndent, 1385 final int subsequentLineIndent, 1386 final int wrapColumn, 1387 final boolean endWithNewline, 1388 final Object... msg) 1389 { 1390 write(out, firstLineIndent, subsequentLineIndent, wrapColumn, 1391 endWithNewline, msg); 1392 } 1393 1394 1395 1396 /** 1397 * Retrieves the print stream that will be used for standard error. 1398 * 1399 * @return The print stream that will be used for standard error. 1400 */ 1401 public final PrintStream getErr() 1402 { 1403 return err; 1404 } 1405 1406 1407 1408 /** 1409 * Retrieves the print stream that may be used to write to the original 1410 * standard error. This may be different from the current standard error 1411 * stream if an output file has been configured. 1412 * 1413 * @return The print stream that may be used to write to the original 1414 * standard error. 1415 */ 1416 public final PrintStream getOriginalErr() 1417 { 1418 return originalErr; 1419 } 1420 1421 1422 1423 /** 1424 * Writes the provided message to the standard error stream for this tool. 1425 * <BR><BR> 1426 * This method is completely threadsafe and my be invoked concurrently by any 1427 * number of threads. 1428 * 1429 * @param msg The message components that will be written to the standard 1430 * error stream. They will be concatenated together on the same 1431 * line, and that line will be followed by an end-of-line 1432 * sequence. 1433 */ 1434 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1435 public final synchronized void err(final Object... msg) 1436 { 1437 write(err, 0, 0, msg); 1438 } 1439 1440 1441 1442 /** 1443 * Writes the provided message to the standard error stream for this tool, 1444 * optionally wrapping and/or indenting the text in the process. 1445 * <BR><BR> 1446 * This method is completely threadsafe and my be invoked concurrently by any 1447 * number of threads. 1448 * 1449 * @param indent The number of spaces each line should be indented. A 1450 * value less than or equal to zero indicates that no 1451 * indent should be used. 1452 * @param wrapColumn The column at which to wrap long lines. A value less 1453 * than or equal to two indicates that no wrapping should 1454 * be performed. If both an indent and a wrap column are 1455 * to be used, then the wrap column must be greater than 1456 * the indent. 1457 * @param msg The message components that will be written to the 1458 * standard output stream. They will be concatenated 1459 * together on the same line, and that line will be 1460 * followed by an end-of-line sequence. 1461 */ 1462 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 1463 public final synchronized void wrapErr(final int indent, final int wrapColumn, 1464 final Object... msg) 1465 { 1466 write(err, indent, wrapColumn, msg); 1467 } 1468 1469 1470 1471 /** 1472 * Writes the provided message to the given print stream, optionally wrapping 1473 * and/or indenting the text in the process. 1474 * 1475 * @param stream The stream to which the message should be written. 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 private static void write(final PrintStream stream, final int indent, 1490 final int wrapColumn, final Object... msg) 1491 { 1492 write(stream, indent, indent, wrapColumn, true, msg); 1493 } 1494 1495 1496 1497 /** 1498 * Writes the provided message to the given print stream, optionally wrapping 1499 * and/or indenting the text in the process. 1500 * 1501 * @param stream The stream to which the message should be 1502 * written. 1503 * @param firstLineIndent The number of spaces the first line should be 1504 * indented. A value less than or equal to zero 1505 * indicates that no indent should be used. 1506 * @param subsequentLineIndent The number of spaces all lines after the 1507 * first should be indented. A value less than 1508 * or equal to zero indicates that no indent 1509 * should be used. 1510 * @param wrapColumn The column at which to wrap long lines. A 1511 * value less than or equal to two indicates 1512 * that no wrapping should be performed. If 1513 * both an indent and a wrap column are to be 1514 * used, then the wrap column must be greater 1515 * than the indent. 1516 * @param endWithNewline Indicates whether a newline sequence should 1517 * follow the last line that is printed. 1518 * @param msg The message components that will be written 1519 * to the standard output stream. They will be 1520 * concatenated together on the same line, and 1521 * that line will be followed by an end-of-line 1522 * sequence. 1523 */ 1524 private static void write(final PrintStream stream, final int firstLineIndent, 1525 final int subsequentLineIndent, 1526 final int wrapColumn, 1527 final boolean endWithNewline, final Object... msg) 1528 { 1529 final StringBuilder buffer = new StringBuilder(); 1530 for (final Object o : msg) 1531 { 1532 buffer.append(o); 1533 } 1534 1535 if (wrapColumn > 2) 1536 { 1537 boolean firstLine = true; 1538 for (final String line : 1539 wrapLine(buffer.toString(), (wrapColumn - firstLineIndent), 1540 (wrapColumn - subsequentLineIndent))) 1541 { 1542 final int indent; 1543 if (firstLine) 1544 { 1545 indent = firstLineIndent; 1546 firstLine = false; 1547 } 1548 else 1549 { 1550 stream.println(); 1551 indent = subsequentLineIndent; 1552 } 1553 1554 if (indent > 0) 1555 { 1556 for (int i=0; i < indent; i++) 1557 { 1558 stream.print(' '); 1559 } 1560 } 1561 stream.print(line); 1562 } 1563 } 1564 else 1565 { 1566 if (firstLineIndent > 0) 1567 { 1568 for (int i=0; i < firstLineIndent; i++) 1569 { 1570 stream.print(' '); 1571 } 1572 } 1573 stream.print(buffer.toString()); 1574 } 1575 1576 if (endWithNewline) 1577 { 1578 stream.println(); 1579 } 1580 stream.flush(); 1581 } 1582}