001/* 002 * Copyright 2008-2016 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2016 UnboundID Corp. 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.OutputStream; 027import java.io.PrintStream; 028import java.util.Collections; 029import java.util.LinkedHashMap; 030import java.util.LinkedHashSet; 031import java.util.Map; 032import java.util.Set; 033import java.util.concurrent.atomic.AtomicReference; 034 035import com.unboundid.ldap.sdk.LDAPException; 036import com.unboundid.ldap.sdk.ResultCode; 037import com.unboundid.util.args.ArgumentException; 038import com.unboundid.util.args.ArgumentParser; 039import com.unboundid.util.args.BooleanArgument; 040 041import static com.unboundid.util.Debug.*; 042import static com.unboundid.util.StaticUtils.*; 043import static com.unboundid.util.UtilityMessages.*; 044 045 046 047/** 048 * This class provides a framework for developing command-line tools that use 049 * the argument parser provided as part of the UnboundID LDAP SDK for Java. 050 * This tool adds a "-H" or "--help" option, which can be used to display usage 051 * information for the program, and may also add a "-V" or "--version" option, 052 * which can display the tool version. 053 * <BR><BR> 054 * Subclasses should include their own {@code main} method that creates an 055 * instance of a {@code CommandLineTool} and should invoke the 056 * {@link CommandLineTool#runTool} method with the provided arguments. For 057 * example: 058 * <PRE> 059 * public class ExampleCommandLineTool 060 * extends CommandLineTool 061 * { 062 * public static void main(String[] args) 063 * { 064 * ExampleCommandLineTool tool = new ExampleCommandLineTool(); 065 * ResultCode resultCode = tool.runTool(args); 066 * if (resultCode != ResultCode.SUCCESS) 067 * { 068 * System.exit(resultCode.intValue()); 069 * } 070 * | 071 * 072 * public ExampleCommandLineTool() 073 * { 074 * super(System.out, System.err); 075 * } 076 * 077 * // The rest of the tool implementation goes here. 078 * ... 079 * } 080 * </PRE>. 081 * <BR><BR> 082 * Note that in general, methods in this class are not threadsafe. However, the 083 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked 084 * concurrently by any number of threads. 085 */ 086@Extensible() 087@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 088public abstract class CommandLineTool 089{ 090 // The print stream to use for messages written to standard output. 091 private final PrintStream out; 092 093 // The print stream to use for messages written to standard error. 094 private final PrintStream err; 095 096 // The argument used to request tool help. 097 private BooleanArgument helpArgument = null; 098 099 // The argument used to request help about SASL authentication. 100 private BooleanArgument helpSASLArgument = null; 101 102 // The argument used to request interactive mode. 103 private BooleanArgument interactiveArgument = null; 104 105 // The argument used to request the tool version. 106 private BooleanArgument versionArgument = null; 107 108 109 110 /** 111 * Creates a new instance of this command-line tool with the provided 112 * information. 113 * 114 * @param outStream The output stream to use for standard output. It may be 115 * {@code System.out} for the JVM's default standard output 116 * stream, {@code null} if no output should be generated, 117 * or a custom output stream if the output should be sent 118 * to an alternate location. 119 * @param errStream The output stream to use for standard error. It may be 120 * {@code System.err} for the JVM's default standard error 121 * stream, {@code null} if no output should be generated, 122 * or a custom output stream if the output should be sent 123 * to an alternate location. 124 */ 125 public CommandLineTool(final OutputStream outStream, 126 final OutputStream errStream) 127 { 128 if (outStream == null) 129 { 130 out = NullOutputStream.getPrintStream(); 131 } 132 else 133 { 134 out = new PrintStream(outStream); 135 } 136 137 if (errStream == null) 138 { 139 err = NullOutputStream.getPrintStream(); 140 } 141 else 142 { 143 err = new PrintStream(errStream); 144 } 145 } 146 147 148 149 /** 150 * Performs all processing for this command-line tool. This includes: 151 * <UL> 152 * <LI>Creating the argument parser and populating it using the 153 * {@link #addToolArguments} method.</LI> 154 * <LI>Parsing the provided set of command line arguments, including any 155 * additional validation using the {@link #doExtendedArgumentValidation} 156 * method.</LI> 157 * <LI>Invoking the {@link #doToolProcessing} method to do the appropriate 158 * work for this tool.</LI> 159 * </UL> 160 * 161 * @param args The command-line arguments provided to this program. 162 * 163 * @return The result of processing this tool. It should be 164 * {@link ResultCode#SUCCESS} if the tool completed its work 165 * successfully, or some other result if a problem occurred. 166 */ 167 public final ResultCode runTool(final String... args) 168 { 169 try 170 { 171 final ArgumentParser parser = createArgumentParser(); 172 if (supportsInteractiveMode() && defaultsToInteractiveMode() && 173 ((args == null) || (args.length == 0))) 174 { 175 // We'll skip argument parsing in this case because no arguments were 176 // provided, and the tool may not allow no arguments to be provided in 177 // non-interactive mode. 178 } 179 else 180 { 181 parser.parse(args); 182 } 183 184 final File generatedPropertiesFile = parser.getGeneratedPropertiesFile(); 185 if (supportsPropertiesFile() && (generatedPropertiesFile != null)) 186 { 187 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1, 188 INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get( 189 generatedPropertiesFile.getAbsolutePath())); 190 return ResultCode.SUCCESS; 191 } 192 193 if (helpArgument.isPresent()) 194 { 195 out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 196 displayExampleUsages(); 197 return ResultCode.SUCCESS; 198 } 199 200 if ((helpSASLArgument != null) && helpSASLArgument.isPresent()) 201 { 202 out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 203 return ResultCode.SUCCESS; 204 } 205 206 if ((versionArgument != null) && versionArgument.isPresent()) 207 { 208 out(getToolVersion()); 209 return ResultCode.SUCCESS; 210 } 211 212 boolean extendedValidationDone = false; 213 if (interactiveArgument != null) 214 { 215 if (interactiveArgument.isPresent() || 216 (defaultsToInteractiveMode() && 217 ((args == null) || (args.length == 0)))) 218 { 219 final CommandLineToolInteractiveModeProcessor interactiveProcessor = 220 new CommandLineToolInteractiveModeProcessor(this, parser); 221 try 222 { 223 interactiveProcessor.doInteractiveModeProcessing(); 224 extendedValidationDone = true; 225 } 226 catch (final LDAPException le) 227 { 228 debugException(le); 229 230 final String message = le.getMessage(); 231 if ((message != null) && (message.length() > 0)) 232 { 233 err(message); 234 } 235 236 return le.getResultCode(); 237 } 238 } 239 } 240 241 if (! extendedValidationDone) 242 { 243 doExtendedArgumentValidation(); 244 } 245 } 246 catch (ArgumentException ae) 247 { 248 debugException(ae); 249 err(ae.getMessage()); 250 return ResultCode.PARAM_ERROR; 251 } 252 253 254 final AtomicReference<ResultCode> exitCode = 255 new AtomicReference<ResultCode>(); 256 if (registerShutdownHook()) 257 { 258 final CommandLineToolShutdownHook shutdownHook = 259 new CommandLineToolShutdownHook(this, exitCode); 260 Runtime.getRuntime().addShutdownHook(shutdownHook); 261 } 262 263 try 264 { 265 exitCode.set(doToolProcessing()); 266 } 267 catch (Exception e) 268 { 269 debugException(e); 270 err(getExceptionMessage(e)); 271 exitCode.set(ResultCode.LOCAL_ERROR); 272 } 273 274 return exitCode.get(); 275 } 276 277 278 279 /** 280 * Writes example usage information for this tool to the standard output 281 * stream. 282 */ 283 private void displayExampleUsages() 284 { 285 final LinkedHashMap<String[],String> examples = getExampleUsages(); 286 if ((examples == null) || examples.isEmpty()) 287 { 288 return; 289 } 290 291 out(INFO_CL_TOOL_LABEL_EXAMPLES); 292 293 final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 294 for (final Map.Entry<String[],String> e : examples.entrySet()) 295 { 296 out(); 297 wrapOut(2, wrapWidth, e.getValue()); 298 out(); 299 300 final StringBuilder buffer = new StringBuilder(); 301 buffer.append(" "); 302 buffer.append(getToolName()); 303 304 final String[] args = e.getKey(); 305 for (int i=0; i < args.length; i++) 306 { 307 buffer.append(' '); 308 309 // If the argument has a value, then make sure to keep it on the same 310 // line as the argument name. This may introduce false positives due to 311 // unnamed trailing arguments, but the worst that will happen that case 312 // is that the output may be wrapped earlier than necessary one time. 313 String arg = args[i]; 314 if (arg.startsWith("-")) 315 { 316 if ((i < (args.length - 1)) && (! args[i+1].startsWith("-"))) 317 { 318 ExampleCommandLineArgument cleanArg = 319 ExampleCommandLineArgument.getCleanArgument(args[i+1]); 320 arg += ' ' + cleanArg.getLocalForm(); 321 i++; 322 } 323 } 324 else 325 { 326 ExampleCommandLineArgument cleanArg = 327 ExampleCommandLineArgument.getCleanArgument(arg); 328 arg = cleanArg.getLocalForm(); 329 } 330 331 if ((buffer.length() + arg.length() + 2) < wrapWidth) 332 { 333 buffer.append(arg); 334 } 335 else 336 { 337 buffer.append('\\'); 338 out(buffer.toString()); 339 buffer.setLength(0); 340 buffer.append(" "); 341 buffer.append(arg); 342 } 343 } 344 345 out(buffer.toString()); 346 } 347 } 348 349 350 351 /** 352 * Retrieves the name of this tool. It should be the name of the command used 353 * to invoke this tool. 354 * 355 * @return The name for this tool. 356 */ 357 public abstract String getToolName(); 358 359 360 361 /** 362 * Retrieves a human-readable description for this tool. 363 * 364 * @return A human-readable description for this tool. 365 */ 366 public abstract String getToolDescription(); 367 368 369 370 /** 371 * Retrieves a version string for this tool, if available. 372 * 373 * @return A version string for this tool, or {@code null} if none is 374 * available. 375 */ 376 public String getToolVersion() 377 { 378 return null; 379 } 380 381 382 383 /** 384 * Retrieves the minimum number of unnamed trailing arguments that must be 385 * provided for this tool. If a tool requires the use of trailing arguments, 386 * then it must override this method and the {@link #getMaxTrailingArguments} 387 * arguments to return nonzero values, and it must also override the 388 * {@link #getTrailingArgumentsPlaceholder} method to return a 389 * non-{@code null} value. 390 * 391 * @return The minimum number of unnamed trailing arguments that may be 392 * provided for this tool. A value of zero indicates that the tool 393 * may be invoked without any trailing arguments. 394 */ 395 public int getMinTrailingArguments() 396 { 397 return 0; 398 } 399 400 401 402 /** 403 * Retrieves the maximum number of unnamed trailing arguments that may be 404 * provided for this tool. If a tool supports trailing arguments, then it 405 * must override this method to return a nonzero value, and must also override 406 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to 407 * return a non-{@code null} value. 408 * 409 * @return The maximum number of unnamed trailing arguments that may be 410 * provided for this tool. A value of zero indicates that trailing 411 * arguments are not allowed. A negative value indicates that there 412 * should be no limit on the number of trailing arguments. 413 */ 414 public int getMaxTrailingArguments() 415 { 416 return 0; 417 } 418 419 420 421 /** 422 * Retrieves a placeholder string that should be used for trailing arguments 423 * in the usage information for this tool. 424 * 425 * @return A placeholder string that should be used for trailing arguments in 426 * the usage information for this tool, or {@code null} if trailing 427 * arguments are not supported. 428 */ 429 public String getTrailingArgumentsPlaceholder() 430 { 431 return null; 432 } 433 434 435 436 /** 437 * Indicates whether this tool should provide support for an interactive mode, 438 * in which the tool offers a mode in which the arguments can be provided in 439 * a text-driven menu rather than requiring them to be given on the command 440 * line. If interactive mode is supported, it may be invoked using the 441 * "--interactive" argument. Alternately, if interactive mode is supported 442 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 443 * interactive mode may be invoked by simply launching the tool without any 444 * arguments. 445 * 446 * @return {@code true} if this tool supports interactive mode, or 447 * {@code false} if not. 448 */ 449 public boolean supportsInteractiveMode() 450 { 451 return false; 452 } 453 454 455 456 /** 457 * Indicates whether this tool defaults to launching in interactive mode if 458 * the tool is invoked without any command-line arguments. This will only be 459 * used if {@link #supportsInteractiveMode()} returns {@code true}. 460 * 461 * @return {@code true} if this tool defaults to using interactive mode if 462 * launched without any command-line arguments, or {@code false} if 463 * not. 464 */ 465 public boolean defaultsToInteractiveMode() 466 { 467 return false; 468 } 469 470 471 472 /** 473 * Indicates whether this tool supports the use of a properties file for 474 * specifying default values for arguments that aren't specified on the 475 * command line. 476 * 477 * @return {@code true} if this tool supports the use of a properties file 478 * for specifying default values for arguments that aren't specified 479 * on the command line, or {@code false} if not. 480 */ 481 public boolean supportsPropertiesFile() 482 { 483 return false; 484 } 485 486 487 488 /** 489 * Creates a parser that can be used to to parse arguments accepted by 490 * this tool. 491 * 492 * @return ArgumentParser that can be used to parse arguments for this 493 * tool. 494 * 495 * @throws ArgumentException If there was a problem initializing the 496 * parser for this tool. 497 */ 498 public final ArgumentParser createArgumentParser() 499 throws ArgumentException 500 { 501 final ArgumentParser parser = new ArgumentParser(getToolName(), 502 getToolDescription(), getMinTrailingArguments(), 503 getMaxTrailingArguments(), getTrailingArgumentsPlaceholder()); 504 505 addToolArguments(parser); 506 507 if (supportsInteractiveMode()) 508 { 509 interactiveArgument = new BooleanArgument(null, "interactive", 510 INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get()); 511 interactiveArgument.setUsageArgument(true); 512 parser.addArgument(interactiveArgument); 513 } 514 515 helpArgument = new BooleanArgument('H', "help", 516 INFO_CL_TOOL_DESCRIPTION_HELP.get()); 517 helpArgument.addShortIdentifier('?'); 518 helpArgument.setUsageArgument(true); 519 parser.addArgument(helpArgument); 520 521 final String version = getToolVersion(); 522 if ((version != null) && (version.length() > 0) && 523 (parser.getNamedArgument("version") == null)) 524 { 525 final Character shortIdentifier; 526 if (parser.getNamedArgument('V') == null) 527 { 528 shortIdentifier = 'V'; 529 } 530 else 531 { 532 shortIdentifier = null; 533 } 534 535 versionArgument = new BooleanArgument(shortIdentifier, "version", 536 INFO_CL_TOOL_DESCRIPTION_VERSION.get()); 537 versionArgument.setUsageArgument(true); 538 parser.addArgument(versionArgument); 539 } 540 541 if (supportsPropertiesFile()) 542 { 543 parser.enablePropertiesFileSupport(); 544 } 545 546 return parser; 547 } 548 549 550 551 /** 552 * Specifies the argument that is used to retrieve usage information about 553 * SASL authentication. 554 * 555 * @param helpSASLArgument The argument that is used to retrieve usage 556 * information about SASL authentication. 557 */ 558 void setHelpSASLArgument(final BooleanArgument helpSASLArgument) 559 { 560 this.helpSASLArgument = helpSASLArgument; 561 } 562 563 564 565 /** 566 * Retrieves a set containing the long identifiers used for LDAP-related 567 * arguments injected by this class. 568 * 569 * @return A set containing the long identifiers used for LDAP-related 570 * arguments injected by this class. 571 */ 572 static Set<String> getUsageArgumentIdentifiers() 573 { 574 final LinkedHashSet<String> ids = new LinkedHashSet<String>(21); 575 576 ids.add("interactive"); 577 ids.add("help"); 578 ids.add("version"); 579 580 return Collections.unmodifiableSet(ids); 581 } 582 583 584 585 /** 586 * Adds the command-line arguments supported for use with this tool to the 587 * provided argument parser. The tool may need to retain references to the 588 * arguments (and/or the argument parser, if trailing arguments are allowed) 589 * to it in order to obtain their values for use in later processing. 590 * 591 * @param parser The argument parser to which the arguments are to be added. 592 * 593 * @throws ArgumentException If a problem occurs while adding any of the 594 * tool-specific arguments to the provided 595 * argument parser. 596 */ 597 public abstract void addToolArguments(final ArgumentParser parser) 598 throws ArgumentException; 599 600 601 602 /** 603 * Performs any necessary processing that should be done to ensure that the 604 * provided set of command-line arguments were valid. This method will be 605 * called after the basic argument parsing has been performed and immediately 606 * before the {@link CommandLineTool#doToolProcessing} method is invoked. 607 * Note that if the tool supports interactive mode, then this method may be 608 * invoked multiple times to allow the user to interactively fix validation 609 * errors. 610 * 611 * @throws ArgumentException If there was a problem with the command-line 612 * arguments provided to this program. 613 */ 614 public void doExtendedArgumentValidation() 615 throws ArgumentException 616 { 617 // No processing will be performed by default. 618 } 619 620 621 622 /** 623 * Performs the core set of processing for this tool. 624 * 625 * @return A result code that indicates whether the processing completed 626 * successfully. 627 */ 628 public abstract ResultCode doToolProcessing(); 629 630 631 632 /** 633 * Indicates whether this tool should register a shutdown hook with the JVM. 634 * Shutdown hooks allow for a best-effort attempt to perform a specified set 635 * of processing when the JVM is shutting down under various conditions, 636 * including: 637 * <UL> 638 * <LI>When all non-daemon threads have stopped running (i.e., the tool has 639 * completed processing).</LI> 640 * <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI> 641 * <LI>When the JVM receives an external kill signal (e.g., via the use of 642 * the kill tool or interrupting the JVM with Ctrl+C).</LI> 643 * </UL> 644 * Shutdown hooks may not be invoked if the process is forcefully killed 645 * (e.g., using "kill -9", or the {@code System.halt()} or 646 * {@code Runtime.halt()} methods). 647 * <BR><BR> 648 * If this method is overridden to return {@code true}, then the 649 * {@link #doShutdownHookProcessing(ResultCode)} method should also be 650 * overridden to contain the logic that will be invoked when the JVM is 651 * shutting down in a manner that calls shutdown hooks. 652 * 653 * @return {@code true} if this tool should register a shutdown hook, or 654 * {@code false} if not. 655 */ 656 protected boolean registerShutdownHook() 657 { 658 return false; 659 } 660 661 662 663 /** 664 * Performs any processing that may be needed when the JVM is shutting down, 665 * whether because tool processing has completed or because it has been 666 * interrupted (e.g., by a kill or break signal). 667 * <BR><BR> 668 * Note that because shutdown hooks run at a delicate time in the life of the 669 * JVM, they should complete quickly and minimize access to external 670 * resources. See the documentation for the 671 * {@code java.lang.Runtime.addShutdownHook} method for recommendations and 672 * restrictions about writing shutdown hooks. 673 * 674 * @param resultCode The result code returned by the tool. It may be 675 * {@code null} if the tool was interrupted before it 676 * completed processing. 677 */ 678 protected void doShutdownHookProcessing(final ResultCode resultCode) 679 { 680 throw new LDAPSDKUsageException( 681 ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get( 682 getToolName())); 683 } 684 685 686 687 /** 688 * Retrieves a set of information that may be used to generate example usage 689 * information. Each element in the returned map should consist of a map 690 * between an example set of arguments and a string that describes the 691 * behavior of the tool when invoked with that set of arguments. 692 * 693 * @return A set of information that may be used to generate example usage 694 * information. It may be {@code null} or empty if no example usage 695 * information is available. 696 */ 697 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 698 public LinkedHashMap<String[],String> getExampleUsages() 699 { 700 return null; 701 } 702 703 704 705 /** 706 * Retrieves the print writer that will be used for standard output. 707 * 708 * @return The print writer that will be used for standard output. 709 */ 710 public final PrintStream getOut() 711 { 712 return out; 713 } 714 715 716 717 /** 718 * Writes the provided message to the standard output stream for this tool. 719 * <BR><BR> 720 * This method is completely threadsafe and my be invoked concurrently by any 721 * number of threads. 722 * 723 * @param msg The message components that will be written to the standard 724 * output stream. They will be concatenated together on the same 725 * line, and that line will be followed by an end-of-line 726 * sequence. 727 */ 728 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 729 public final synchronized void out(final Object... msg) 730 { 731 write(out, 0, 0, msg); 732 } 733 734 735 736 /** 737 * Writes the provided message to the standard output stream for this tool, 738 * optionally wrapping and/or indenting the text in the process. 739 * <BR><BR> 740 * This method is completely threadsafe and my be invoked concurrently by any 741 * number of threads. 742 * 743 * @param indent The number of spaces each line should be indented. A 744 * value less than or equal to zero indicates that no 745 * indent should be used. 746 * @param wrapColumn The column at which to wrap long lines. A value less 747 * than or equal to two indicates that no wrapping should 748 * be performed. If both an indent and a wrap column are 749 * to be used, then the wrap column must be greater than 750 * the indent. 751 * @param msg The message components that will be written to the 752 * standard output stream. They will be concatenated 753 * together on the same line, and that line will be 754 * followed by an end-of-line sequence. 755 */ 756 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 757 public final synchronized void wrapOut(final int indent, final int wrapColumn, 758 final Object... msg) 759 { 760 write(out, indent, wrapColumn, msg); 761 } 762 763 764 765 /** 766 * Writes the provided message to the standard output stream for this tool, 767 * optionally wrapping and/or indenting the text in the process. 768 * <BR><BR> 769 * This method is completely threadsafe and my be invoked concurrently by any 770 * number of threads. 771 * 772 * @param firstLineIndent The number of spaces the first line should be 773 * indented. A value less than or equal to zero 774 * indicates that no indent should be used. 775 * @param subsequentLineIndent The number of spaces each line except the 776 * first should be indented. A value less than 777 * or equal to zero indicates that no indent 778 * should be used. 779 * @param wrapColumn The column at which to wrap long lines. A 780 * value less than or equal to two indicates 781 * that no wrapping should be performed. If 782 * both an indent and a wrap column are to be 783 * used, then the wrap column must be greater 784 * than the indent. 785 * @param endWithNewline Indicates whether a newline sequence should 786 * follow the last line that is printed. 787 * @param msg The message components that will be written 788 * to the standard output stream. They will be 789 * concatenated together on the same line, and 790 * that line will be followed by an end-of-line 791 * sequence. 792 */ 793 final synchronized void wrapStandardOut(final int firstLineIndent, 794 final int subsequentLineIndent, 795 final int wrapColumn, 796 final boolean endWithNewline, 797 final Object... msg) 798 { 799 write(out, firstLineIndent, subsequentLineIndent, wrapColumn, 800 endWithNewline, msg); 801 } 802 803 804 805 /** 806 * Retrieves the print writer that will be used for standard error. 807 * 808 * @return The print writer that will be used for standard error. 809 */ 810 public final PrintStream getErr() 811 { 812 return err; 813 } 814 815 816 817 /** 818 * Writes the provided message to the standard error stream for this tool. 819 * <BR><BR> 820 * This method is completely threadsafe and my be invoked concurrently by any 821 * number of threads. 822 * 823 * @param msg The message components that will be written to the standard 824 * error stream. They will be concatenated together on the same 825 * line, and that line will be followed by an end-of-line 826 * sequence. 827 */ 828 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 829 public final synchronized void err(final Object... msg) 830 { 831 write(err, 0, 0, msg); 832 } 833 834 835 836 /** 837 * Writes the provided message to the standard error stream for this tool, 838 * optionally wrapping and/or indenting the text in the process. 839 * <BR><BR> 840 * This method is completely threadsafe and my be invoked concurrently by any 841 * number of threads. 842 * 843 * @param indent The number of spaces each line should be indented. A 844 * value less than or equal to zero indicates that no 845 * indent should be used. 846 * @param wrapColumn The column at which to wrap long lines. A value less 847 * than or equal to two indicates that no wrapping should 848 * be performed. If both an indent and a wrap column are 849 * to be used, then the wrap column must be greater than 850 * the indent. 851 * @param msg The message components that will be written to the 852 * standard output stream. They will be concatenated 853 * together on the same line, and that line will be 854 * followed by an end-of-line sequence. 855 */ 856 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 857 public final synchronized void wrapErr(final int indent, final int wrapColumn, 858 final Object... msg) 859 { 860 write(err, indent, wrapColumn, msg); 861 } 862 863 864 865 /** 866 * Writes the provided message to the given print stream, optionally wrapping 867 * and/or indenting the text in the process. 868 * 869 * @param stream The stream to which the message should be written. 870 * @param indent The number of spaces each line should be indented. A 871 * value less than or equal to zero indicates that no 872 * indent should be used. 873 * @param wrapColumn The column at which to wrap long lines. A value less 874 * than or equal to two indicates that no wrapping should 875 * be performed. If both an indent and a wrap column are 876 * to be used, then the wrap column must be greater than 877 * the indent. 878 * @param msg The message components that will be written to the 879 * standard output stream. They will be concatenated 880 * together on the same line, and that line will be 881 * followed by an end-of-line sequence. 882 */ 883 private static void write(final PrintStream stream, final int indent, 884 final int wrapColumn, final Object... msg) 885 { 886 write(stream, indent, indent, wrapColumn, true, msg); 887 } 888 889 890 891 /** 892 * Writes the provided message to the given print stream, optionally wrapping 893 * and/or indenting the text in the process. 894 * 895 * @param stream The stream to which the message should be 896 * written. 897 * @param firstLineIndent The number of spaces the first line should be 898 * indented. A value less than or equal to zero 899 * indicates that no indent should be used. 900 * @param subsequentLineIndent The number of spaces all lines after the 901 * first should be indented. A value less than 902 * or equal to zero indicates that no indent 903 * should be used. 904 * @param wrapColumn The column at which to wrap long lines. A 905 * value less than or equal to two indicates 906 * that no wrapping should be performed. If 907 * both an indent and a wrap column are to be 908 * used, then the wrap column must be greater 909 * than the indent. 910 * @param endWithNewline Indicates whether a newline sequence should 911 * follow the last line that is printed. 912 * @param msg The message components that will be written 913 * to the standard output stream. They will be 914 * concatenated together on the same line, and 915 * that line will be followed by an end-of-line 916 * sequence. 917 */ 918 private static void write(final PrintStream stream, final int firstLineIndent, 919 final int subsequentLineIndent, 920 final int wrapColumn, 921 final boolean endWithNewline, final Object... msg) 922 { 923 final StringBuilder buffer = new StringBuilder(); 924 for (final Object o : msg) 925 { 926 buffer.append(o); 927 } 928 929 if (wrapColumn > 2) 930 { 931 boolean firstLine = true; 932 for (final String line : 933 wrapLine(buffer.toString(), (wrapColumn - firstLineIndent), 934 (wrapColumn - subsequentLineIndent))) 935 { 936 final int indent; 937 if (firstLine) 938 { 939 indent = firstLineIndent; 940 firstLine = false; 941 } 942 else 943 { 944 stream.println(); 945 indent = subsequentLineIndent; 946 } 947 948 if (indent > 0) 949 { 950 for (int i=0; i < indent; i++) 951 { 952 stream.print(' '); 953 } 954 } 955 stream.print(line); 956 } 957 } 958 else 959 { 960 if (firstLineIndent > 0) 961 { 962 for (int i=0; i < firstLineIndent; i++) 963 { 964 stream.print(' '); 965 } 966 } 967 stream.print(buffer.toString()); 968 } 969 970 if (endWithNewline) 971 { 972 stream.println(); 973 } 974 stream.flush(); 975 } 976}