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