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