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 */
021 package com.unboundid.util;
022
023
024
025 import java.io.OutputStream;
026 import java.io.PrintStream;
027 import java.util.LinkedHashMap;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.concurrent.atomic.AtomicReference;
031
032 import com.unboundid.ldap.sdk.ResultCode;
033 import com.unboundid.util.args.ArgumentException;
034 import com.unboundid.util.args.ArgumentParser;
035 import com.unboundid.util.args.BooleanArgument;
036
037 import static com.unboundid.util.Debug.*;
038 import static com.unboundid.util.StaticUtils.*;
039 import 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)
084 public 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 }