001/* 002 * Copyright 2008-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2014 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util; 022 023 024 025import java.io.OutputStream; 026import java.io.PrintStream; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.concurrent.atomic.AtomicReference; 031 032import com.unboundid.ldap.sdk.ResultCode; 033import com.unboundid.util.args.ArgumentException; 034import com.unboundid.util.args.ArgumentParser; 035import com.unboundid.util.args.BooleanArgument; 036 037import static com.unboundid.util.Debug.*; 038import static com.unboundid.util.StaticUtils.*; 039import static com.unboundid.util.UtilityMessages.*; 040 041 042 043/** 044 * This class provides a framework for developing command-line tools that use 045 * the argument parser provided as part of the UnboundID LDAP SDK for Java. 046 * This tool adds a "-H" or "--help" option, which can be used to display usage 047 * information for the program, and may also add a "-V" or "--version" option, 048 * which can display the tool version. 049 * <BR><BR> 050 * Subclasses should include their own {@code main} method that creates an 051 * instance of a {@code CommandLineTool} and should invoke the 052 * {@link CommandLineTool#runTool} method with the provided arguments. For 053 * example: 054 * <PRE> 055 * public class ExampleCommandLineTool 056 * extends CommandLineTool 057 * { 058 * public static void main(String[] args) 059 * { 060 * ExampleCommandLineTool tool = new ExampleCommandLineTool(); 061 * ResultCode resultCode = tool.runTool(args); 062 * if (resultCode != ResultCode.SUCCESS) 063 * { 064 * System.exit(resultCode.intValue()); 065 * } 066 * | 067 * 068 * public ExampleCommandLineTool() 069 * { 070 * super(System.out, System.err); 071 * } 072 * 073 * // The rest of the tool implementation goes here. 074 * ... 075 * } 076 * </PRE>. 077 * <BR><BR> 078 * Note that in general, methods in this class are not threadsafe. However, the 079 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked 080 * concurrently by any number of threads. 081 */ 082@Extensible() 083@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 084public abstract class CommandLineTool 085{ 086 // The print stream to use for messages written to standard output. 087 private final PrintStream out; 088 089 // The print stream to use for messages written to standard error. 090 private final PrintStream err; 091 092 // The argument used to request tool help. 093 private BooleanArgument helpArgument = null; 094 095 // The argument used to request the tool version. 096 private BooleanArgument versionArgument = null; 097 098 099 100 /** 101 * Creates a new instance of this command-line tool with the provided 102 * information. 103 * 104 * @param outStream The output stream to use for standard output. It may be 105 * {@code System.out} for the JVM's default standard output 106 * stream, {@code null} if no output should be generated, 107 * or a custom output stream if the output should be sent 108 * to an alternate location. 109 * @param errStream The output stream to use for standard error. It may be 110 * {@code System.err} for the JVM's default standard error 111 * stream, {@code null} if no output should be generated, 112 * or a custom output stream if the output should be sent 113 * to an alternate location. 114 */ 115 public CommandLineTool(final OutputStream outStream, 116 final OutputStream errStream) 117 { 118 if (outStream == null) 119 { 120 out = NullOutputStream.getPrintStream(); 121 } 122 else 123 { 124 out = new PrintStream(outStream); 125 } 126 127 if (errStream == null) 128 { 129 err = NullOutputStream.getPrintStream(); 130 } 131 else 132 { 133 err = new PrintStream(errStream); 134 } 135 } 136 137 138 139 /** 140 * Performs all processing for this command-line tool. This includes: 141 * <UL> 142 * <LI>Creating the argument parser and populating it using the 143 * {@link #addToolArguments} method.</LI> 144 * <LI>Parsing the provided set of command line arguments, including any 145 * additional validation using the {@link #doExtendedArgumentValidation} 146 * method.</LI> 147 * <LI>Invoking the {@link #doToolProcessing} method to do the appropriate 148 * work for this tool.</LI> 149 * </UL> 150 * 151 * @param args The command-line arguments provided to this program. 152 * 153 * @return The result of processing this tool. It should be 154 * {@link ResultCode#SUCCESS} if the tool completed its work 155 * successfully, or some other result if a problem occurred. 156 */ 157 public final ResultCode runTool(final String... args) 158 { 159 try 160 { 161 final ArgumentParser parser = createArgumentParser(); 162 parser.parse(args); 163 164 if (helpArgument.isPresent()) 165 { 166 out(parser.getUsageString(79)); 167 displayExampleUsages(); 168 return ResultCode.SUCCESS; 169 } 170 171 if ((versionArgument != null) && versionArgument.isPresent()) 172 { 173 out(getToolVersion()); 174 return ResultCode.SUCCESS; 175 } 176 177 doExtendedArgumentValidation(); 178 } 179 catch (ArgumentException ae) 180 { 181 debugException(ae); 182 err(ae.getMessage()); 183 return ResultCode.PARAM_ERROR; 184 } 185 186 187 final AtomicReference<ResultCode> exitCode = 188 new AtomicReference<ResultCode>(); 189 if (registerShutdownHook()) 190 { 191 final CommandLineToolShutdownHook shutdownHook = 192 new CommandLineToolShutdownHook(this, exitCode); 193 Runtime.getRuntime().addShutdownHook(shutdownHook); 194 } 195 196 try 197 { 198 exitCode.set(doToolProcessing()); 199 } 200 catch (Exception e) 201 { 202 debugException(e); 203 err(getExceptionMessage(e)); 204 exitCode.set(ResultCode.LOCAL_ERROR); 205 } 206 207 return exitCode.get(); 208 } 209 210 211 212 /** 213 * Writes example usage information for this tool to the standard output 214 * stream. 215 */ 216 private void displayExampleUsages() 217 { 218 final LinkedHashMap<String[],String> examples = getExampleUsages(); 219 if ((examples == null) || examples.isEmpty()) 220 { 221 return; 222 } 223 224 out(INFO_CL_TOOL_LABEL_EXAMPLES); 225 226 for (final Map.Entry<String[],String> e : examples.entrySet()) 227 { 228 out(); 229 wrapOut(2, 79, e.getValue()); 230 out(); 231 232 final StringBuilder buffer = new StringBuilder(); 233 buffer.append(" "); 234 buffer.append(getToolName()); 235 236 final String[] args = e.getKey(); 237 for (int i=0; i < args.length; i++) 238 { 239 buffer.append(' '); 240 241 // If the argument has a value, then make sure to keep it on the same 242 // line as the argument name. This may introduce false positives due to 243 // unnamed trailing arguments, but the worst that will happen that case 244 // is that the output may be wrapped earlier than necessary one time. 245 String arg = args[i]; 246 if (arg.startsWith("-")) 247 { 248 if ((i < (args.length - 1)) && (! args[i+1].startsWith("-"))) 249 { 250 ExampleCommandLineArgument cleanArg = 251 ExampleCommandLineArgument.getCleanArgument(args[i+1]); 252 arg += ' ' + cleanArg.getLocalForm(); 253 i++; 254 } 255 } 256 else 257 { 258 ExampleCommandLineArgument cleanArg = 259 ExampleCommandLineArgument.getCleanArgument(arg); 260 arg = cleanArg.getLocalForm(); 261 } 262 263 if ((buffer.length() + arg.length() + 2) < 79) 264 { 265 buffer.append(arg); 266 } 267 else 268 { 269 buffer.append('\\'); 270 out(buffer.toString()); 271 buffer.setLength(0); 272 buffer.append(" "); 273 buffer.append(arg); 274 } 275 } 276 277 out(buffer.toString()); 278 } 279 } 280 281 282 283 /** 284 * Retrieves the name of this tool. It should be the name of the command used 285 * to invoke this tool. 286 * 287 * @return The name for this tool. 288 */ 289 public abstract String getToolName(); 290 291 292 293 /** 294 * Retrieves a human-readable description for this tool. 295 * 296 * @return A human-readable description for this tool. 297 */ 298 public abstract String getToolDescription(); 299 300 301 302 /** 303 * Retrieves a version string for this tool, if available. 304 * 305 * @return A version string for this tool, or {@code null} if none is 306 * available. 307 */ 308 public String getToolVersion() 309 { 310 return null; 311 } 312 313 314 315 /** 316 * Retrieves the maximum number of unnamed trailing arguments that may be 317 * provided for this tool. If a tool supports trailing arguments, then it 318 * must override this method to return a nonzero value, and must also override 319 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to 320 * return a non-{@code null} value. 321 * 322 * @return The maximum number of unnamed trailing arguments that may be 323 * provided for this tool. A value of zero indicates that trailing 324 * arguments are not allowed. A negative value indicates that there 325 * should be no limit on the number of trailing arguments. 326 */ 327 public int getMaxTrailingArguments() 328 { 329 return 0; 330 } 331 332 333 334 /** 335 * Retrieves a placeholder string that should be used for trailing arguments 336 * in the usage information for this tool. 337 * 338 * @return A placeholder string that should be used for trailing arguments in 339 * the usage information for this tool, or {@code null} if trailing 340 * arguments are not supported. 341 */ 342 public String getTrailingArgumentsPlaceholder() 343 { 344 return null; 345 } 346 347 348 349 /** 350 * Creates a parser that can be used to to parse arguments accepted by 351 * this tool. 352 * 353 * @return ArgumentParser that can be used to parse arguments for this 354 * tool. 355 * 356 * @throws ArgumentException If there was a problem initializing the 357 * parser for this tool. 358 */ 359 public final ArgumentParser createArgumentParser() 360 throws ArgumentException 361 { 362 final ArgumentParser parser = new ArgumentParser(getToolName(), 363 getToolDescription(), getMaxTrailingArguments(), 364 getTrailingArgumentsPlaceholder()); 365 366 addToolArguments(parser); 367 368 helpArgument = new BooleanArgument('H', "help", 369 INFO_CL_TOOL_DESCRIPTION_HELP.get()); 370 helpArgument.addShortIdentifier('?'); 371 helpArgument.setUsageArgument(true); 372 parser.addArgument(helpArgument); 373 374 final String version = getToolVersion(); 375 if ((version != null) && (version.length() > 0) && 376 (parser.getNamedArgument("version") == null)) 377 { 378 final Character shortIdentifier; 379 if (parser.getNamedArgument('V') == null) 380 { 381 shortIdentifier = 'V'; 382 } 383 else 384 { 385 shortIdentifier = null; 386 } 387 388 versionArgument = new BooleanArgument(shortIdentifier, "version", 389 INFO_CL_TOOL_DESCRIPTION_VERSION.get()); 390 versionArgument.setUsageArgument(true); 391 parser.addArgument(versionArgument); 392 } 393 394 return parser; 395 } 396 397 398 399 /** 400 * Adds the command-line arguments supported for use with this tool to the 401 * provided argument parser. The tool may need to retain references to the 402 * arguments (and/or the argument parser, if trailing arguments are allowed) 403 * to it in order to obtain their values for use in later processing. 404 * 405 * @param parser The argument parser to which the arguments are to be added. 406 * 407 * @throws ArgumentException If a problem occurs while adding any of the 408 * tool-specific arguments to the provided 409 * argument parser. 410 */ 411 public abstract void addToolArguments(final ArgumentParser parser) 412 throws ArgumentException; 413 414 415 416 /** 417 * Performs any necessary processing that should be done to ensure that the 418 * provided set of command-line arguments were valid. This method will be 419 * called after the basic argument parsing has been performed and immediately 420 * before the {@link CommandLineTool#doToolProcessing} method is invoked. 421 * 422 * @throws ArgumentException If there was a problem with the command-line 423 * arguments provided to this program. 424 */ 425 public void doExtendedArgumentValidation() 426 throws ArgumentException 427 { 428 // No processing will be performed by default. 429 } 430 431 432 433 /** 434 * Performs the core set of processing for this tool. 435 * 436 * @return A result code that indicates whether the processing completed 437 * successfully. 438 */ 439 public abstract ResultCode doToolProcessing(); 440 441 442 443 /** 444 * Indicates whether this tool should register a shutdown hook with the JVM. 445 * Shutdown hooks allow for a best-effort attempt to perform a specified set 446 * of processing when the JVM is shutting down under various conditions, 447 * including: 448 * <UL> 449 * <LI>When all non-daemon threads have stopped running (i.e., the tool has 450 * completed processing).</LI> 451 * <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI> 452 * <LI>When the JVM receives an external kill signal (e.g., via the use of 453 * the kill tool or interrupting the JVM with Ctrl+C).</LI> 454 * </UL> 455 * Shutdown hooks may not be invoked if the process is forcefully killed 456 * (e.g., using "kill -9", or the {@code System.halt()} or 457 * {@code Runtime.halt()} methods). 458 * <BR><BR> 459 * If this method is overridden to return {@code true}, then the 460 * {@link #doShutdownHookProcessing(ResultCode)} method should also be 461 * overridden to contain the logic that will be invoked when the JVM is 462 * shutting down in a manner that calls shutdown hooks. 463 * 464 * @return {@code true} if this tool should register a shutdown hook, or 465 * {@code false} if not. 466 */ 467 protected boolean registerShutdownHook() 468 { 469 return false; 470 } 471 472 473 474 /** 475 * Performs any processing that may be needed when the JVM is shutting down, 476 * whether because tool processing has completed or because it has been 477 * interrupted (e.g., by a kill or break signal). 478 * <BR><BR> 479 * Note that because shutdown hooks run at a delicate time in the life of the 480 * JVM, they should complete quickly and minimize access to external 481 * resources. See the documentation for the 482 * {@code java.lang.Runtime.addShutdownHook} method for recommendations and 483 * restrictions about writing shutdown hooks. 484 * 485 * @param resultCode The result code returned by the tool. It may be 486 * {@code null} if the tool was interrupted before it 487 * completed processing. 488 */ 489 protected void doShutdownHookProcessing(final ResultCode resultCode) 490 { 491 throw new LDAPSDKUsageException( 492 ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get( 493 getToolName())); 494 } 495 496 497 498 /** 499 * Retrieves a set of information that may be used to generate example usage 500 * information. Each element in the returned map should consist of a map 501 * between an example set of arguments and a string that describes the 502 * behavior of the tool when invoked with that set of arguments. 503 * 504 * @return A set of information that may be used to generate example usage 505 * information. It may be {@code null} or empty if no example usage 506 * information is available. 507 */ 508 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 509 public LinkedHashMap<String[],String> getExampleUsages() 510 { 511 return null; 512 } 513 514 515 516 /** 517 * Retrieves the print writer that will be used for standard output. 518 * 519 * @return The print writer that will be used for standard output. 520 */ 521 public final PrintStream getOut() 522 { 523 return out; 524 } 525 526 527 528 /** 529 * Writes the provided message to the standard output stream for this tool. 530 * <BR><BR> 531 * This method is completely threadsafe and my be invoked concurrently by any 532 * number of threads. 533 * 534 * @param msg The message components that will be written to the standard 535 * output stream. They will be concatenated together on the same 536 * line, and that line will be followed by an end-of-line 537 * sequence. 538 */ 539 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 540 public final synchronized void out(final Object... msg) 541 { 542 write(out, 0, 0, msg); 543 } 544 545 546 547 /** 548 * Writes the provided message to the standard output stream for this tool, 549 * optionally wrapping and/or indenting the text in the process. 550 * <BR><BR> 551 * This method is completely threadsafe and my be invoked concurrently by any 552 * number of threads. 553 * 554 * @param indent The number of spaces each line should be indented. A 555 * value less than or equal to zero indicates that no 556 * indent should be used. 557 * @param wrapColumn The column at which to wrap long lines. A value less 558 * than or equal to two indicates that no wrapping should 559 * be performed. If both an indent and a wrap column are 560 * to be used, then the wrap column must be greater than 561 * the indent. 562 * @param msg The message components that will be written to the 563 * standard output stream. They will be concatenated 564 * together on the same line, and that line will be 565 * followed by an end-of-line sequence. 566 */ 567 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 568 public final synchronized void wrapOut(final int indent, final int wrapColumn, 569 final Object... msg) 570 { 571 write(out, indent, wrapColumn, msg); 572 } 573 574 575 576 /** 577 * Retrieves the print writer that will be used for standard error. 578 * 579 * @return The print writer that will be used for standard error. 580 */ 581 public final PrintStream getErr() 582 { 583 return err; 584 } 585 586 587 588 /** 589 * Writes the provided message to the standard error stream for this tool. 590 * <BR><BR> 591 * This method is completely threadsafe and my be invoked concurrently by any 592 * number of threads. 593 * 594 * @param msg The message components that will be written to the standard 595 * error stream. They will be concatenated together on the same 596 * line, and that line will be followed by an end-of-line 597 * sequence. 598 */ 599 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 600 public final synchronized void err(final Object... msg) 601 { 602 write(err, 0, 0, msg); 603 } 604 605 606 607 /** 608 * Writes the provided message to the standard error stream for this tool, 609 * optionally wrapping and/or indenting the text in the process. 610 * <BR><BR> 611 * This method is completely threadsafe and my be invoked concurrently by any 612 * number of threads. 613 * 614 * @param indent The number of spaces each line should be indented. A 615 * value less than or equal to zero indicates that no 616 * indent should be used. 617 * @param wrapColumn The column at which to wrap long lines. A value less 618 * than or equal to two indicates that no wrapping should 619 * be performed. If both an indent and a wrap column are 620 * to be used, then the wrap column must be greater than 621 * the indent. 622 * @param msg The message components that will be written to the 623 * standard output stream. They will be concatenated 624 * together on the same line, and that line will be 625 * followed by an end-of-line sequence. 626 */ 627 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE) 628 public final synchronized void wrapErr(final int indent, final int wrapColumn, 629 final Object... msg) 630 { 631 write(err, indent, wrapColumn, msg); 632 } 633 634 635 636 /** 637 * Writes the provided message to the given print stream, optionally wrapping 638 * and/or indenting the text in the process. 639 * 640 * @param stream The stream to which the message should be written. 641 * @param indent The number of spaces each line should be indented. A 642 * value less than or equal to zero indicates that no 643 * indent should be used. 644 * @param wrapColumn The column at which to wrap long lines. A value less 645 * than or equal to two indicates that no wrapping should 646 * be performed. If both an indent and a wrap column are 647 * to be used, then the wrap column must be greater than 648 * the indent. 649 * @param msg The message components that will be written to the 650 * standard output stream. They will be concatenated 651 * together on the same line, and that line will be 652 * followed by an end-of-line sequence. 653 */ 654 private static void write(final PrintStream stream, final int indent, 655 final int wrapColumn, final Object... msg) 656 { 657 final StringBuilder buffer = new StringBuilder(); 658 for (final Object o : msg) 659 { 660 buffer.append(o); 661 } 662 663 if (wrapColumn > 2) 664 { 665 final List<String> lines; 666 if (indent > 0) 667 { 668 for (final String line : 669 wrapLine(buffer.toString(), (wrapColumn - indent))) 670 { 671 for (int i=0; i < indent; i++) 672 { 673 stream.print(' '); 674 } 675 stream.println(line); 676 } 677 } 678 else 679 { 680 for (final String line : wrapLine(buffer.toString(), wrapColumn)) 681 { 682 stream.println(line); 683 } 684 } 685 } 686 else 687 { 688 if (indent > 0) 689 { 690 for (int i=0; i < indent; i++) 691 { 692 stream.print(' '); 693 } 694 } 695 stream.println(buffer.toString()); 696 } 697 } 698}