001/* 002 * Copyright 2008-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util; 022 023 024 025import java.io.File; 026import java.io.FileOutputStream; 027import java.io.OutputStream; 028import java.io.PrintStream; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.HashSet; 032import java.util.Iterator; 033import java.util.LinkedHashMap; 034import java.util.LinkedHashSet; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038import java.util.TreeMap; 039import java.util.concurrent.atomic.AtomicReference; 040 041import com.unboundid.ldap.sdk.LDAPException; 042import com.unboundid.ldap.sdk.ResultCode; 043import com.unboundid.util.args.Argument; 044import com.unboundid.util.args.ArgumentException; 045import com.unboundid.util.args.ArgumentParser; 046import com.unboundid.util.args.BooleanArgument; 047import com.unboundid.util.args.FileArgument; 048import com.unboundid.util.args.SubCommand; 049import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogger; 050import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogDetails; 051import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogShutdownHook; 052 053import static com.unboundid.util.UtilityMessages.*; 054 055 056 057/** 058 * This class provides a framework for developing command-line tools that use 059 * the argument parser provided as part of the UnboundID LDAP SDK for Java. 060 * This tool adds a "-H" or "--help" option, which can be used to display usage 061 * information for the program, and may also add a "-V" or "--version" option, 062 * which can display the tool version. 063 * <BR><BR> 064 * Subclasses should include their own {@code main} method that creates an 065 * instance of a {@code CommandLineTool} and should invoke the 066 * {@link CommandLineTool#runTool} method with the provided arguments. For 067 * example: 068 * <PRE> 069 * public class ExampleCommandLineTool 070 * extends CommandLineTool 071 * { 072 * public static void main(String[] args) 073 * { 074 * ExampleCommandLineTool tool = new ExampleCommandLineTool(); 075 * ResultCode resultCode = tool.runTool(args); 076 * if (resultCode != ResultCode.SUCCESS) 077 * { 078 * System.exit(resultCode.intValue()); 079 * } 080 * | 081 * 082 * public ExampleCommandLineTool() 083 * { 084 * super(System.out, System.err); 085 * } 086 * 087 * // The rest of the tool implementation goes here. 088 * ... 089 * } 090 * </PRE>. 091 * <BR><BR> 092 * Note that in general, methods in this class are not threadsafe. However, the 093 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked 094 * concurrently by any number of threads. 095 */ 096@Extensible() 097@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 098public abstract class CommandLineTool 099{ 100 // The print stream that was originally used for standard output. It may not 101 // be the current standard output stream if an output file has been 102 // configured. 103 private final PrintStream originalOut; 104 105 // The print stream that was originally used for standard error. It may not 106 // be the current standard error stream if an output file has been configured. 107 private final PrintStream originalErr; 108 109 // The print stream to use for messages written to standard output. 110 private volatile PrintStream out; 111 112 // The print stream to use for messages written to standard error. 113 private volatile PrintStream err; 114 115 // The argument used to indicate that the tool should append to the output 116 // file rather than overwrite it. 117 private BooleanArgument appendToOutputFileArgument = null; 118 119 // The argument used to request tool help. 120 private BooleanArgument helpArgument = null; 121 122 // The argument used to request help about SASL authentication. 123 private BooleanArgument helpSASLArgument = null; 124 125 // The argument used to request help information about all of the subcommands. 126 private BooleanArgument helpSubcommandsArgument = null; 127 128 // The argument used to request interactive mode. 129 private BooleanArgument interactiveArgument = null; 130 131 // The argument used to indicate that output should be written to standard out 132 // as well as the specified output file. 133 private BooleanArgument teeOutputArgument = null; 134 135 // The argument used to request the tool version. 136 private BooleanArgument versionArgument = null; 137 138 // The argument used to specify the output file for standard output and 139 // standard error. 140 private FileArgument outputFileArgument = null; 141 142 143 144 /** 145 * Creates a new instance of this command-line tool with the provided 146 * information. 147 * 148 * @param outStream The output stream to use for standard output. It may be 149 * {@code System.out} for the JVM's default standard output 150 * stream, {@code null} if no output should be generated, 151 * or a custom output stream if the output should be sent 152 * to an alternate location. 153 * @param errStream The output stream to use for standard error. It may be 154 * {@code System.err} for the JVM's default standard error 155 * stream, {@code null} if no output should be generated, 156 * or a custom output stream if the output should be sent 157 * to an alternate location. 158 */ 159 public CommandLineTool(final OutputStream outStream, 160 final OutputStream errStream) 161 { 162 if (outStream == null) 163 { 164 out = NullOutputStream.getPrintStream(); 165 } 166 else 167 { 168 out = new PrintStream(outStream); 169 } 170 171 if (errStream == null) 172 { 173 err = NullOutputStream.getPrintStream(); 174 } 175 else 176 { 177 err = new PrintStream(errStream); 178 } 179 180 originalOut = out; 181 originalErr = err; 182 } 183 184 185 186 /** 187 * Performs all processing for this command-line tool. This includes: 188 * <UL> 189 * <LI>Creating the argument parser and populating it using the 190 * {@link #addToolArguments} method.</LI> 191 * <LI>Parsing the provided set of command line arguments, including any 192 * additional validation using the {@link #doExtendedArgumentValidation} 193 * method.</LI> 194 * <LI>Invoking the {@link #doToolProcessing} method to do the appropriate 195 * work for this tool.</LI> 196 * </UL> 197 * 198 * @param args The command-line arguments provided to this program. 199 * 200 * @return The result of processing this tool. It should be 201 * {@link ResultCode#SUCCESS} if the tool completed its work 202 * successfully, or some other result if a problem occurred. 203 */ 204 public final ResultCode runTool(final String... args) 205 { 206 final ArgumentParser parser; 207 try 208 { 209 parser = createArgumentParser(); 210 boolean exceptionFromParsingWithNoArgumentsExplicitlyProvided = false; 211 if (supportsInteractiveMode() && defaultsToInteractiveMode() && 212 ((args == null) || (args.length == 0))) 213 { 214 // We'll go ahead and perform argument parsing even though no arguments 215 // were provided because there might be a properties file that should 216 // prevent running in interactive mode. But we'll ignore any exception 217 // thrown during argument parsing because the tool might require 218 // arguments when run non-interactively. 219 try 220 { 221 parser.parse(args); 222 } 223 catch (final Exception e) 224 { 225 Debug.debugException(e); 226 exceptionFromParsingWithNoArgumentsExplicitlyProvided = true; 227 } 228 } 229 else 230 { 231 parser.parse(args); 232 } 233 234 final File generatedPropertiesFile = parser.getGeneratedPropertiesFile(); 235 if (supportsPropertiesFile() && (generatedPropertiesFile != null)) 236 { 237 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1, 238 INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get( 239 generatedPropertiesFile.getAbsolutePath())); 240 return ResultCode.SUCCESS; 241 } 242 243 if (helpArgument.isPresent()) 244 { 245 out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 246 displayExampleUsages(parser); 247 return ResultCode.SUCCESS; 248 } 249 250 if ((helpSASLArgument != null) && helpSASLArgument.isPresent()) 251 { 252 out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 253 return ResultCode.SUCCESS; 254 } 255 256 if ((helpSubcommandsArgument != null) && 257 helpSubcommandsArgument.isPresent()) 258 { 259 final TreeMap<String,SubCommand> subCommands = 260 getSortedSubCommands(parser); 261 for (final SubCommand sc : subCommands.values()) 262 { 263 final StringBuilder nameBuffer = new StringBuilder(); 264 265 final Iterator<String> nameIterator = sc.getNames(false).iterator(); 266 while (nameIterator.hasNext()) 267 { 268 nameBuffer.append(nameIterator.next()); 269 if (nameIterator.hasNext()) 270 { 271 nameBuffer.append(", "); 272 } 273 } 274 out(nameBuffer.toString()); 275 276 for (final String descriptionLine : 277 StaticUtils.wrapLine(sc.getDescription(), 278 (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3))) 279 { 280 out(" " + descriptionLine); 281 } 282 out(); 283 } 284 285 wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1), 286 INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName())); 287 return ResultCode.SUCCESS; 288 } 289 290 if ((versionArgument != null) && versionArgument.isPresent()) 291 { 292 out(getToolVersion()); 293 return ResultCode.SUCCESS; 294 } 295 296 boolean extendedValidationDone = false; 297 if (interactiveArgument != null) 298 { 299 if (interactiveArgument.isPresent() || 300 (defaultsToInteractiveMode() && 301 ((args == null) || (args.length == 0)) && 302 (parser.getArgumentsSetFromPropertiesFile().isEmpty() || 303 exceptionFromParsingWithNoArgumentsExplicitlyProvided))) 304 { 305 final CommandLineToolInteractiveModeProcessor interactiveProcessor = 306 new CommandLineToolInteractiveModeProcessor(this, parser); 307 try 308 { 309 interactiveProcessor.doInteractiveModeProcessing(); 310 extendedValidationDone = true; 311 } 312 catch (final LDAPException le) 313 { 314 Debug.debugException(le); 315 316 final String message = le.getMessage(); 317 if ((message != null) && (! message.isEmpty())) 318 { 319 err(message); 320 } 321 322 return le.getResultCode(); 323 } 324 } 325 } 326 327 if (! extendedValidationDone) 328 { 329 doExtendedArgumentValidation(); 330 } 331 } 332 catch (final ArgumentException ae) 333 { 334 Debug.debugException(ae); 335 err(ae.getMessage()); 336 return ResultCode.PARAM_ERROR; 337 } 338 339 if ((outputFileArgument != null) && outputFileArgument.isPresent()) 340 { 341 final File outputFile = outputFileArgument.getValue(); 342 final boolean append = ((appendToOutputFileArgument != null) && 343 appendToOutputFileArgument.isPresent()); 344 345 final PrintStream outputFileStream; 346 try 347 { 348 final FileOutputStream fos = new FileOutputStream(outputFile, append); 349 outputFileStream = new PrintStream(fos, true, "UTF-8"); 350 } 351 catch (final Exception e) 352 { 353 Debug.debugException(e); 354 err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get( 355 outputFile.getAbsolutePath(), StaticUtils.getExceptionMessage(e))); 356 return ResultCode.LOCAL_ERROR; 357 } 358 359 if ((teeOutputArgument != null) && teeOutputArgument.isPresent()) 360 { 361 out = new PrintStream(new TeeOutputStream(out, outputFileStream)); 362 err = new PrintStream(new TeeOutputStream(err, outputFileStream)); 363 } 364 else 365 { 366 out = outputFileStream; 367 err = outputFileStream; 368 } 369 } 370 371 372 // If any values were selected using a properties file, then display 373 // information about them. 374 final List<String> argsSetFromPropertiesFiles = 375 parser.getArgumentsSetFromPropertiesFile(); 376 if ((! argsSetFromPropertiesFiles.isEmpty()) && 377 (! parser.suppressPropertiesFileComment())) 378 { 379 for (final String line : 380 StaticUtils.wrapLine( 381 INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get( 382 parser.getPropertiesFileUsed().getPath()), 383 (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3))) 384 { 385 out("# ", line); 386 } 387 388 final StringBuilder buffer = new StringBuilder(); 389 for (final String s : argsSetFromPropertiesFiles) 390 { 391 if (s.startsWith("-")) 392 { 393 if (buffer.length() > 0) 394 { 395 out(buffer); 396 buffer.setLength(0); 397 } 398 399 buffer.append("# "); 400 buffer.append(s); 401 } 402 else 403 { 404 if (buffer.length() == 0) 405 { 406 // This should never happen. 407 buffer.append("# "); 408 } 409 else 410 { 411 buffer.append(' '); 412 } 413 414 buffer.append(StaticUtils.cleanExampleCommandLineArgument(s)); 415 } 416 } 417 418 if (buffer.length() > 0) 419 { 420 out(buffer); 421 } 422 423 out(); 424 } 425 426 427 CommandLineToolShutdownHook shutdownHook = null; 428 final AtomicReference<ResultCode> exitCode = new AtomicReference<>(); 429 if (registerShutdownHook()) 430 { 431 shutdownHook = new CommandLineToolShutdownHook(this, exitCode); 432 Runtime.getRuntime().addShutdownHook(shutdownHook); 433 } 434 435 final ToolInvocationLogDetails logDetails = 436 ToolInvocationLogger.getLogMessageDetails( 437 getToolName(), logToolInvocationByDefault(), getErr()); 438 ToolInvocationLogShutdownHook logShutdownHook = null; 439 440 if (logDetails.logInvocation()) 441 { 442 final HashSet<Argument> argumentsSetFromPropertiesFile = 443 new HashSet<>(10); 444 final ArrayList<ObjectPair<String,String>> propertiesFileArgList = 445 new ArrayList<>(10); 446 getToolInvocationPropertiesFileArguments(parser, 447 argumentsSetFromPropertiesFile, propertiesFileArgList); 448 449 final ArrayList<ObjectPair<String,String>> providedArgList = 450 new ArrayList<>(10); 451 getToolInvocationProvidedArguments(parser, 452 argumentsSetFromPropertiesFile, providedArgList); 453 454 logShutdownHook = new ToolInvocationLogShutdownHook(logDetails); 455 Runtime.getRuntime().addShutdownHook(logShutdownHook); 456 457 final String propertiesFilePath; 458 if (propertiesFileArgList.isEmpty()) 459 { 460 propertiesFilePath = ""; 461 } 462 else 463 { 464 final File propertiesFile = parser.getPropertiesFileUsed(); 465 if (propertiesFile == null) 466 { 467 propertiesFilePath = ""; 468 } 469 else 470 { 471 propertiesFilePath = propertiesFile.getAbsolutePath(); 472 } 473 } 474 475 ToolInvocationLogger.logLaunchMessage(logDetails, providedArgList, 476 propertiesFileArgList, propertiesFilePath); 477 } 478 479 try 480 { 481 exitCode.set(doToolProcessing()); 482 } 483 catch (final Exception e) 484 { 485 Debug.debugException(e); 486 err(StaticUtils.getExceptionMessage(e)); 487 exitCode.set(ResultCode.LOCAL_ERROR); 488 } 489 finally 490 { 491 if (logShutdownHook != null) 492 { 493 Runtime.getRuntime().removeShutdownHook(logShutdownHook); 494 495 String completionMessage = getToolCompletionMessage(); 496 if (completionMessage == null) 497 { 498 completionMessage = exitCode.get().getName(); 499 } 500 501 ToolInvocationLogger.logCompletionMessage( 502 logDetails, exitCode.get().intValue(), completionMessage); 503 } 504 if (shutdownHook != null) 505 { 506 Runtime.getRuntime().removeShutdownHook(shutdownHook); 507 } 508 } 509 510 return exitCode.get(); 511 } 512 513 514 515 /** 516 * Updates the provided argument list with object pairs that comprise the 517 * set of arguments actually provided to this tool on the command line. 518 * 519 * @param parser The argument parser for this tool. 520 * It must not be {@code null}. 521 * @param argumentsSetFromPropertiesFile A set that includes all arguments 522 * set from the properties file. 523 * @param argList The list to which the argument 524 * information should be added. It 525 * must not be {@code null}. The 526 * first element of each object pair 527 * that is added must be 528 * non-{@code null}. The second 529 * element in any given pair may be 530 * {@code null} if the first element 531 * represents the name of an argument 532 * that doesn't take any values, the 533 * name of the selected subcommand, or 534 * an unnamed trailing argument. 535 */ 536 private static void getToolInvocationProvidedArguments( 537 final ArgumentParser parser, 538 final Set<Argument> argumentsSetFromPropertiesFile, 539 final List<ObjectPair<String,String>> argList) 540 { 541 final String noValue = null; 542 final SubCommand subCommand = parser.getSelectedSubCommand(); 543 if (subCommand != null) 544 { 545 argList.add(new ObjectPair<>(subCommand.getPrimaryName(), noValue)); 546 } 547 548 for (final Argument arg : parser.getNamedArguments()) 549 { 550 // Exclude arguments that weren't provided. 551 if (! arg.isPresent()) 552 { 553 continue; 554 } 555 556 // Exclude arguments that were set from the properties file. 557 if (argumentsSetFromPropertiesFile.contains(arg)) 558 { 559 continue; 560 } 561 562 if (arg.takesValue()) 563 { 564 for (final String value : arg.getValueStringRepresentations(false)) 565 { 566 if (arg.isSensitive()) 567 { 568 argList.add(new ObjectPair<>(arg.getIdentifierString(), 569 "*****REDACTED*****")); 570 } 571 else 572 { 573 argList.add(new ObjectPair<>(arg.getIdentifierString(), value)); 574 } 575 } 576 } 577 else 578 { 579 argList.add(new ObjectPair<>(arg.getIdentifierString(), noValue)); 580 } 581 } 582 583 if (subCommand != null) 584 { 585 getToolInvocationProvidedArguments(subCommand.getArgumentParser(), 586 argumentsSetFromPropertiesFile, argList); 587 } 588 589 for (final String trailingArgument : parser.getTrailingArguments()) 590 { 591 argList.add(new ObjectPair<>(trailingArgument, noValue)); 592 } 593 } 594 595 596 597 /** 598 * Updates the provided argument list with object pairs that comprise the 599 * set of tool arguments set from a properties file. 600 * 601 * @param parser The argument parser for this tool. 602 * It must not be {@code null}. 603 * @param argumentsSetFromPropertiesFile A set that should be updated with 604 * each argument set from the 605 * properties file. 606 * @param argList The list to which the argument 607 * information should be added. It 608 * must not be {@code null}. The 609 * first element of each object pair 610 * that is added must be 611 * non-{@code null}. The second 612 * element in any given pair may be 613 * {@code null} if the first element 614 * represents the name of an argument 615 * that doesn't take any values, the 616 * name of the selected subcommand, or 617 * an unnamed trailing argument. 618 */ 619 private static void getToolInvocationPropertiesFileArguments( 620 final ArgumentParser parser, 621 final Set<Argument> argumentsSetFromPropertiesFile, 622 final List<ObjectPair<String,String>> argList) 623 { 624 final ArgumentParser subCommandParser; 625 final SubCommand subCommand = parser.getSelectedSubCommand(); 626 if (subCommand == null) 627 { 628 subCommandParser = null; 629 } 630 else 631 { 632 subCommandParser = subCommand.getArgumentParser(); 633 } 634 635 final String noValue = null; 636 637 final Iterator<String> iterator = 638 parser.getArgumentsSetFromPropertiesFile().iterator(); 639 while (iterator.hasNext()) 640 { 641 final String arg = iterator.next(); 642 if (arg.startsWith("-")) 643 { 644 Argument a; 645 if (arg.startsWith("--")) 646 { 647 final String longIdentifier = arg.substring(2); 648 a = parser.getNamedArgument(longIdentifier); 649 if ((a == null) && (subCommandParser != null)) 650 { 651 a = subCommandParser.getNamedArgument(longIdentifier); 652 } 653 } 654 else 655 { 656 final char shortIdentifier = arg.charAt(1); 657 a = parser.getNamedArgument(shortIdentifier); 658 if ((a == null) && (subCommandParser != null)) 659 { 660 a = subCommandParser.getNamedArgument(shortIdentifier); 661 } 662 } 663 664 if (a != null) 665 { 666 argumentsSetFromPropertiesFile.add(a); 667 668 if (a.takesValue()) 669 { 670 final String value = iterator.next(); 671 if (a.isSensitive()) 672 { 673 argList.add(new ObjectPair<>(a.getIdentifierString(), noValue)); 674 } 675 else 676 { 677 argList.add(new ObjectPair<>(a.getIdentifierString(), value)); 678 } 679 } 680 else 681 { 682 argList.add(new ObjectPair<>(a.getIdentifierString(), noValue)); 683 } 684 } 685 } 686 else 687 { 688 argList.add(new ObjectPair<>(arg, noValue)); 689 } 690 } 691 } 692 693 694 695 /** 696 * Retrieves a sorted map of subcommands for the provided argument parser, 697 * alphabetized by primary name. 698 * 699 * @param parser The argument parser for which to get the sorted 700 * subcommands. 701 * 702 * @return The sorted map of subcommands. 703 */ 704 private static TreeMap<String,SubCommand> getSortedSubCommands( 705 final ArgumentParser parser) 706 { 707 final TreeMap<String,SubCommand> m = new TreeMap<>(); 708 for (final SubCommand sc : parser.getSubCommands()) 709 { 710 m.put(sc.getPrimaryName(), sc); 711 } 712 return m; 713 } 714 715 716 717 /** 718 * Writes example usage information for this tool to the standard output 719 * stream. 720 * 721 * @param parser The argument parser used to process the provided set of 722 * command-line arguments. 723 */ 724 private void displayExampleUsages(final ArgumentParser parser) 725 { 726 final LinkedHashMap<String[],String> examples; 727 if ((parser != null) && (parser.getSelectedSubCommand() != null)) 728 { 729 examples = parser.getSelectedSubCommand().getExampleUsages(); 730 } 731 else 732 { 733 examples = getExampleUsages(); 734 } 735 736 if ((examples == null) || examples.isEmpty()) 737 { 738 return; 739 } 740 741 out(INFO_CL_TOOL_LABEL_EXAMPLES); 742 743 final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 744 for (final Map.Entry<String[],String> e : examples.entrySet()) 745 { 746 out(); 747 wrapOut(2, wrapWidth, e.getValue()); 748 out(); 749 750 final StringBuilder buffer = new StringBuilder(); 751 buffer.append(" "); 752 buffer.append(getToolName()); 753 754 final String[] args = e.getKey(); 755 for (int i=0; i < args.length; i++) 756 { 757 buffer.append(' '); 758 759 // If the argument has a value, then make sure to keep it on the same 760 // line as the argument name. This may introduce false positives due to 761 // unnamed trailing arguments, but the worst that will happen that case 762 // is that the output may be wrapped earlier than necessary one time. 763 String arg = args[i]; 764 if (arg.startsWith("-")) 765 { 766 if ((i < (args.length - 1)) && (! args[i+1].startsWith("-"))) 767 { 768 final ExampleCommandLineArgument cleanArg = 769 ExampleCommandLineArgument.getCleanArgument(args[i+1]); 770 arg += ' ' + cleanArg.getLocalForm(); 771 i++; 772 } 773 } 774 else 775 { 776 final ExampleCommandLineArgument cleanArg = 777 ExampleCommandLineArgument.getCleanArgument(arg); 778 arg = cleanArg.getLocalForm(); 779 } 780 781 if ((buffer.length() + arg.length() + 2) < wrapWidth) 782 { 783 buffer.append(arg); 784 } 785 else 786 { 787 buffer.append('\\'); 788 out(buffer.toString()); 789 buffer.setLength(0); 790 buffer.append(" "); 791 buffer.append(arg); 792 } 793 } 794 795 out(buffer.toString()); 796 } 797 } 798 799 800 801 /** 802 * Retrieves the name of this tool. It should be the name of the command used 803 * to invoke this tool. 804 * 805 * @return The name for this tool. 806 */ 807 public abstract String getToolName(); 808 809 810 811 /** 812 * Retrieves a human-readable description for this tool. 813 * 814 * @return A human-readable description for this tool. 815 */ 816 public abstract String getToolDescription(); 817 818 819 820 /** 821 * Retrieves a version string for this tool, if available. 822 * 823 * @return A version string for this tool, or {@code null} if none is 824 * available. 825 */ 826 public String getToolVersion() 827 { 828 return null; 829 } 830 831 832 833 /** 834 * Retrieves the minimum number of unnamed trailing arguments that must be 835 * provided for this tool. If a tool requires the use of trailing arguments, 836 * then it must override this method and the {@link #getMaxTrailingArguments} 837 * arguments to return nonzero values, and it must also override the 838 * {@link #getTrailingArgumentsPlaceholder} method to return a 839 * non-{@code null} value. 840 * 841 * @return The minimum number of unnamed trailing arguments that may be 842 * provided for this tool. A value of zero indicates that the tool 843 * may be invoked without any trailing arguments. 844 */ 845 public int getMinTrailingArguments() 846 { 847 return 0; 848 } 849 850 851 852 /** 853 * Retrieves the maximum number of unnamed trailing arguments that may be 854 * provided for this tool. If a tool supports trailing arguments, then it 855 * must override this method to return a nonzero value, and must also override 856 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to 857 * return a non-{@code null} value. 858 * 859 * @return The maximum number of unnamed trailing arguments that may be 860 * provided for this tool. A value of zero indicates that trailing 861 * arguments are not allowed. A negative value indicates that there 862 * should be no limit on the number of trailing arguments. 863 */ 864 public int getMaxTrailingArguments() 865 { 866 return 0; 867 } 868 869 870 871 /** 872 * Retrieves a placeholder string that should be used for trailing arguments 873 * in the usage information for this tool. 874 * 875 * @return A placeholder string that should be used for trailing arguments in 876 * the usage information for this tool, or {@code null} if trailing 877 * arguments are not supported. 878 */ 879 public String getTrailingArgumentsPlaceholder() 880 { 881 return null; 882 } 883 884 885 886 /** 887 * Indicates whether this tool should provide support for an interactive mode, 888 * in which the tool offers a mode in which the arguments can be provided in 889 * a text-driven menu rather than requiring them to be given on the command 890 * line. If interactive mode is supported, it may be invoked using the 891 * "--interactive" argument. Alternately, if interactive mode is supported 892 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 893 * interactive mode may be invoked by simply launching the tool without any 894 * arguments. 895 * 896 * @return {@code true} if this tool supports interactive mode, or 897 * {@code false} if not. 898 */ 899 public boolean supportsInteractiveMode() 900 { 901 return false; 902 } 903 904 905 906 /** 907 * Indicates whether this tool defaults to launching in interactive mode if 908 * the tool is invoked without any command-line arguments. This will only be 909 * used if {@link #supportsInteractiveMode()} returns {@code true}. 910 * 911 * @return {@code true} if this tool defaults to using interactive mode if 912 * launched without any command-line arguments, or {@code false} if 913 * not. 914 */ 915 public boolean defaultsToInteractiveMode() 916 { 917 return false; 918 } 919 920 921 922 /** 923 * Indicates whether this tool supports the use of a properties file for 924 * specifying default values for arguments that aren't specified on the 925 * command line. 926 * 927 * @return {@code true} if this tool supports the use of a properties file 928 * for specifying default values for arguments that aren't specified 929 * on the command line, or {@code false} if not. 930 */ 931 public boolean supportsPropertiesFile() 932 { 933 return false; 934 } 935 936 937 938 /** 939 * Indicates whether this tool should provide arguments for redirecting output 940 * to a file. If this method returns {@code true}, then the tool will offer 941 * an "--outputFile" argument that will specify the path to a file to which 942 * all standard output and standard error content will be written, and it will 943 * also offer a "--teeToStandardOut" argument that can only be used if the 944 * "--outputFile" argument is present and will cause all output to be written 945 * to both the specified output file and to standard output. 946 * 947 * @return {@code true} if this tool should provide arguments for redirecting 948 * output to a file, or {@code false} if not. 949 */ 950 protected boolean supportsOutputFile() 951 { 952 return false; 953 } 954 955 956 957 /** 958 * Indicates whether to log messages about the launch and completion of this 959 * tool into the invocation log of Ping Identity server products that may 960 * include it. This method is not needed for tools that are not expected to 961 * be part of the Ping Identity server products suite. Further, this value 962 * may be overridden by settings in the server's 963 * tool-invocation-logging.properties file. 964 * <BR><BR> 965 * This method should generally return {@code true} for tools that may alter 966 * the server configuration, data, or other state information, and 967 * {@code false} for tools that do not make any changes. 968 * 969 * @return {@code true} if Ping Identity server products should include 970 * messages about the launch and completion of this tool in tool 971 * invocation log files by default, or {@code false} if not. 972 */ 973 protected boolean logToolInvocationByDefault() 974 { 975 return false; 976 } 977 978 979 980 /** 981 * Retrieves an optional message that may provide additional information about 982 * the way that the tool completed its processing. For example if the tool 983 * exited with an error message, it may be useful for this method to return 984 * that error message. 985 * <BR><BR> 986 * The message returned by this method is intended for purposes and is not 987 * meant to be parsed or programmatically interpreted. 988 * 989 * @return An optional message that may provide additional information about 990 * the completion state for this tool, or {@code null} if no 991 * completion message is available. 992 */ 993 protected String getToolCompletionMessage() 994 { 995 return null; 996 } 997 998 999 1000 /** 1001 * Creates a parser that can be used to to parse arguments accepted by 1002 * this tool. 1003 * 1004 * @return ArgumentParser that can be used to parse arguments for this 1005 * tool. 1006 * 1007 * @throws ArgumentException If there was a problem initializing the 1008 * parser for this tool. 1009 */ 1010 public final ArgumentParser createArgumentParser() 1011 throws ArgumentException 1012 { 1013 final ArgumentParser parser = new ArgumentParser(getToolName(), 1014 getToolDescription(), getMinTrailingArguments(), 1015 getMaxTrailingArguments(), getTrailingArgumentsPlaceholder()); 1016 1017 addToolArguments(parser); 1018 1019 if (supportsInteractiveMode()) 1020 { 1021 interactiveArgument = new BooleanArgument(null, "interactive", 1022 INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get()); 1023 interactiveArgument.setUsageArgument(true); 1024 parser.addArgument(interactiveArgument); 1025 } 1026 1027 if (supportsOutputFile()) 1028 { 1029 outputFileArgument = new FileArgument(null, "outputFile", false, 1, null, 1030 INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true, 1031 false); 1032 outputFileArgument.addLongIdentifier("output-file", true); 1033 outputFileArgument.setUsageArgument(true); 1034 parser.addArgument(outputFileArgument); 1035 1036 appendToOutputFileArgument = new BooleanArgument(null, 1037 "appendToOutputFile", 1, 1038 INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get( 1039 outputFileArgument.getIdentifierString())); 1040 appendToOutputFileArgument.addLongIdentifier("append-to-output-file", 1041 true); 1042 appendToOutputFileArgument.setUsageArgument(true); 1043 parser.addArgument(appendToOutputFileArgument); 1044 1045 teeOutputArgument = new BooleanArgument(null, "teeOutput", 1, 1046 INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get( 1047 outputFileArgument.getIdentifierString())); 1048 teeOutputArgument.addLongIdentifier("tee-output", true); 1049 teeOutputArgument.setUsageArgument(true); 1050 parser.addArgument(teeOutputArgument); 1051 1052 parser.addDependentArgumentSet(appendToOutputFileArgument, 1053 outputFileArgument); 1054 parser.addDependentArgumentSet(teeOutputArgument, 1055 outputFileArgument); 1056 } 1057 1058 helpArgument = new BooleanArgument('H', "help", 1059 INFO_CL_TOOL_DESCRIPTION_HELP.get()); 1060 helpArgument.addShortIdentifier('?', true); 1061 helpArgument.setUsageArgument(true); 1062 parser.addArgument(helpArgument); 1063 1064 if (! parser.getSubCommands().isEmpty()) 1065 { 1066 helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1, 1067 INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get()); 1068 helpSubcommandsArgument.addLongIdentifier("helpSubcommand", true); 1069 helpSubcommandsArgument.addLongIdentifier("help-subcommands", true); 1070 helpSubcommandsArgument.addLongIdentifier("help-subcommand", true); 1071 helpSubcommandsArgument.setUsageArgument(true); 1072 parser.addArgument(helpSubcommandsArgument); 1073 } 1074 1075 final String version = getToolVersion(); 1076 if ((version != null) && (! version.isEmpty()) && 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<>(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 StaticUtils.wrapLine(buffer.toString(), 1540 (wrapColumn - firstLineIndent), 1541 (wrapColumn - subsequentLineIndent))) 1542 { 1543 final int indent; 1544 if (firstLine) 1545 { 1546 indent = firstLineIndent; 1547 firstLine = false; 1548 } 1549 else 1550 { 1551 stream.println(); 1552 indent = subsequentLineIndent; 1553 } 1554 1555 if (indent > 0) 1556 { 1557 for (int i=0; i < indent; i++) 1558 { 1559 stream.print(' '); 1560 } 1561 } 1562 stream.print(line); 1563 } 1564 } 1565 else 1566 { 1567 if (firstLineIndent > 0) 1568 { 1569 for (int i=0; i < firstLineIndent; i++) 1570 { 1571 stream.print(' '); 1572 } 1573 } 1574 stream.print(buffer.toString()); 1575 } 1576 1577 if (endWithNewline) 1578 { 1579 stream.println(); 1580 } 1581 stream.flush(); 1582 } 1583}