001 /*
002 * Copyright 2008-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.util;
022
023
024
025 import java.io.File;
026 import java.io.FileOutputStream;
027 import java.io.OutputStream;
028 import java.io.PrintStream;
029 import java.util.Collections;
030 import java.util.Iterator;
031 import java.util.LinkedHashMap;
032 import java.util.LinkedHashSet;
033 import java.util.List;
034 import java.util.Map;
035 import java.util.Set;
036 import java.util.TreeMap;
037 import java.util.concurrent.atomic.AtomicReference;
038
039 import com.unboundid.ldap.sdk.LDAPException;
040 import com.unboundid.ldap.sdk.ResultCode;
041 import com.unboundid.util.args.ArgumentException;
042 import com.unboundid.util.args.ArgumentParser;
043 import com.unboundid.util.args.BooleanArgument;
044 import com.unboundid.util.args.FileArgument;
045 import com.unboundid.util.args.SubCommand;
046
047 import static com.unboundid.util.Debug.*;
048 import static com.unboundid.util.StaticUtils.*;
049 import static com.unboundid.util.UtilityMessages.*;
050
051
052
053 /**
054 * This class provides a framework for developing command-line tools that use
055 * the argument parser provided as part of the UnboundID LDAP SDK for Java.
056 * This tool adds a "-H" or "--help" option, which can be used to display usage
057 * information for the program, and may also add a "-V" or "--version" option,
058 * which can display the tool version.
059 * <BR><BR>
060 * Subclasses should include their own {@code main} method that creates an
061 * instance of a {@code CommandLineTool} and should invoke the
062 * {@link CommandLineTool#runTool} method with the provided arguments. For
063 * example:
064 * <PRE>
065 * public class ExampleCommandLineTool
066 * extends CommandLineTool
067 * {
068 * public static void main(String[] args)
069 * {
070 * ExampleCommandLineTool tool = new ExampleCommandLineTool();
071 * ResultCode resultCode = tool.runTool(args);
072 * if (resultCode != ResultCode.SUCCESS)
073 * {
074 * System.exit(resultCode.intValue());
075 * }
076 * |
077 *
078 * public ExampleCommandLineTool()
079 * {
080 * super(System.out, System.err);
081 * }
082 *
083 * // The rest of the tool implementation goes here.
084 * ...
085 * }
086 * </PRE>.
087 * <BR><BR>
088 * Note that in general, methods in this class are not threadsafe. However, the
089 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked
090 * concurrently by any number of threads.
091 */
092 @Extensible()
093 @ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
094 public abstract class CommandLineTool
095 {
096 // The print stream that was originally used for standard output. It may not
097 // be the current standard output stream if an output file has been
098 // configured.
099 private final PrintStream originalOut;
100
101 // The print stream that was originally used for standard error. It may not
102 // be the current standard error stream if an output file has been configured.
103 private final PrintStream originalErr;
104
105 // The print stream to use for messages written to standard output.
106 private volatile PrintStream out;
107
108 // The print stream to use for messages written to standard error.
109 private volatile PrintStream err;
110
111 // The argument used to indicate that the tool should append to the output
112 // file rather than overwrite it.
113 private BooleanArgument appendToOutputFileArgument = null;
114
115 // The argument used to request tool help.
116 private BooleanArgument helpArgument = null;
117
118 // The argument used to request help about SASL authentication.
119 private BooleanArgument helpSASLArgument = null;
120
121 // The argument used to request help information about all of the subcommands.
122 private BooleanArgument helpSubcommandsArgument = null;
123
124 // The argument used to request interactive mode.
125 private BooleanArgument interactiveArgument = null;
126
127 // The argument used to indicate that output should be written to standard out
128 // as well as the specified output file.
129 private BooleanArgument teeOutputArgument = null;
130
131 // The argument used to request the tool version.
132 private BooleanArgument versionArgument = null;
133
134 // The argument used to specify the output file for standard output and
135 // standard error.
136 private FileArgument outputFileArgument = null;
137
138
139
140 /**
141 * Creates a new instance of this command-line tool with the provided
142 * information.
143 *
144 * @param outStream The output stream to use for standard output. It may be
145 * {@code System.out} for the JVM's default standard output
146 * stream, {@code null} if no output should be generated,
147 * or a custom output stream if the output should be sent
148 * to an alternate location.
149 * @param errStream The output stream to use for standard error. It may be
150 * {@code System.err} for the JVM's default standard error
151 * stream, {@code null} if no output should be generated,
152 * or a custom output stream if the output should be sent
153 * to an alternate location.
154 */
155 public CommandLineTool(final OutputStream outStream,
156 final OutputStream errStream)
157 {
158 if (outStream == null)
159 {
160 out = NullOutputStream.getPrintStream();
161 }
162 else
163 {
164 out = new PrintStream(outStream);
165 }
166
167 if (errStream == null)
168 {
169 err = NullOutputStream.getPrintStream();
170 }
171 else
172 {
173 err = new PrintStream(errStream);
174 }
175
176 originalOut = out;
177 originalErr = err;
178 }
179
180
181
182 /**
183 * Performs all processing for this command-line tool. This includes:
184 * <UL>
185 * <LI>Creating the argument parser and populating it using the
186 * {@link #addToolArguments} method.</LI>
187 * <LI>Parsing the provided set of command line arguments, including any
188 * additional validation using the {@link #doExtendedArgumentValidation}
189 * method.</LI>
190 * <LI>Invoking the {@link #doToolProcessing} method to do the appropriate
191 * work for this tool.</LI>
192 * </UL>
193 *
194 * @param args The command-line arguments provided to this program.
195 *
196 * @return The result of processing this tool. It should be
197 * {@link ResultCode#SUCCESS} if the tool completed its work
198 * successfully, or some other result if a problem occurred.
199 */
200 public final ResultCode runTool(final String... args)
201 {
202 final ArgumentParser parser;
203 try
204 {
205 parser = createArgumentParser();
206 if (supportsInteractiveMode() && defaultsToInteractiveMode() &&
207 ((args == null) || (args.length == 0)))
208 {
209 // We'll skip argument parsing in this case because no arguments were
210 // provided, and the tool may not allow no arguments to be provided in
211 // non-interactive mode.
212 }
213 else
214 {
215 parser.parse(args);
216 }
217
218 final File generatedPropertiesFile = parser.getGeneratedPropertiesFile();
219 if (supportsPropertiesFile() && (generatedPropertiesFile != null))
220 {
221 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1,
222 INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get(
223 generatedPropertiesFile.getAbsolutePath()));
224 return ResultCode.SUCCESS;
225 }
226
227 if (helpArgument.isPresent())
228 {
229 out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
230 displayExampleUsages(parser);
231 return ResultCode.SUCCESS;
232 }
233
234 if ((helpSASLArgument != null) && helpSASLArgument.isPresent())
235 {
236 out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
237 return ResultCode.SUCCESS;
238 }
239
240 if ((helpSubcommandsArgument != null) &&
241 helpSubcommandsArgument.isPresent())
242 {
243 final TreeMap<String,SubCommand> subCommands =
244 getSortedSubCommands(parser);
245 for (final SubCommand sc : subCommands.values())
246 {
247 final StringBuilder nameBuffer = new StringBuilder();
248
249 final Iterator<String> nameIterator = sc.getNames().iterator();
250 while (nameIterator.hasNext())
251 {
252 nameBuffer.append(nameIterator.next());
253 if (nameIterator.hasNext())
254 {
255 nameBuffer.append(", ");
256 }
257 }
258 out(nameBuffer.toString());
259
260 for (final String descriptionLine :
261 wrapLine(sc.getDescription(),
262 (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3)))
263 {
264 out(" " + descriptionLine);
265 }
266 out();
267 }
268
269 wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1),
270 INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName()));
271 return ResultCode.SUCCESS;
272 }
273
274 if ((versionArgument != null) && versionArgument.isPresent())
275 {
276 out(getToolVersion());
277 return ResultCode.SUCCESS;
278 }
279
280 boolean extendedValidationDone = false;
281 if (interactiveArgument != null)
282 {
283 if (interactiveArgument.isPresent() ||
284 (defaultsToInteractiveMode() &&
285 ((args == null) || (args.length == 0))))
286 {
287 final CommandLineToolInteractiveModeProcessor interactiveProcessor =
288 new CommandLineToolInteractiveModeProcessor(this, parser);
289 try
290 {
291 interactiveProcessor.doInteractiveModeProcessing();
292 extendedValidationDone = true;
293 }
294 catch (final LDAPException le)
295 {
296 debugException(le);
297
298 final String message = le.getMessage();
299 if ((message != null) && (message.length() > 0))
300 {
301 err(message);
302 }
303
304 return le.getResultCode();
305 }
306 }
307 }
308
309 if (! extendedValidationDone)
310 {
311 doExtendedArgumentValidation();
312 }
313 }
314 catch (ArgumentException ae)
315 {
316 debugException(ae);
317 err(ae.getMessage());
318 return ResultCode.PARAM_ERROR;
319 }
320
321 if ((outputFileArgument != null) && outputFileArgument.isPresent())
322 {
323 final File outputFile = outputFileArgument.getValue();
324 final boolean append = ((appendToOutputFileArgument != null) &&
325 appendToOutputFileArgument.isPresent());
326
327 final PrintStream outputFileStream;
328 try
329 {
330 final FileOutputStream fos = new FileOutputStream(outputFile, append);
331 outputFileStream = new PrintStream(fos, true, "UTF-8");
332 }
333 catch (final Exception e)
334 {
335 debugException(e);
336 err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get(
337 outputFile.getAbsolutePath(), getExceptionMessage(e)));
338 return ResultCode.LOCAL_ERROR;
339 }
340
341 if ((teeOutputArgument != null) && teeOutputArgument.isPresent())
342 {
343 out = new PrintStream(new TeeOutputStream(out, outputFileStream));
344 err = new PrintStream(new TeeOutputStream(err, outputFileStream));
345 }
346 else
347 {
348 out = outputFileStream;
349 err = outputFileStream;
350 }
351 }
352
353
354 // If any values were selected using a properties file, then display
355 // information about them.
356 final List<String> argsSetFromPropertiesFiles =
357 parser.getArgumentsSetFromPropertiesFile();
358 if (! argsSetFromPropertiesFiles.isEmpty())
359 {
360 for (final String line :
361 wrapLine(
362 INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get(
363 parser.getPropertiesFileUsed().getPath()),
364 (TERMINAL_WIDTH_COLUMNS - 3)))
365 {
366 out("# ", line);
367 }
368
369 final StringBuilder buffer = new StringBuilder();
370 for (final String s : argsSetFromPropertiesFiles)
371 {
372 if (s.startsWith("-"))
373 {
374 if (buffer.length() > 0)
375 {
376 out(buffer);
377 buffer.setLength(0);
378 }
379
380 buffer.append("# ");
381 buffer.append(s);
382 }
383 else
384 {
385 if (buffer.length() == 0)
386 {
387 // This should never happen.
388 buffer.append("# ");
389 }
390 else
391 {
392 buffer.append(' ');
393 }
394
395 buffer.append(StaticUtils.cleanExampleCommandLineArgument(s));
396 }
397 }
398
399 if (buffer.length() > 0)
400 {
401 out(buffer);
402 }
403
404 out();
405 }
406
407
408 CommandLineToolShutdownHook shutdownHook = null;
409 final AtomicReference<ResultCode> exitCode =
410 new AtomicReference<ResultCode>();
411 if (registerShutdownHook())
412 {
413 shutdownHook = new CommandLineToolShutdownHook(this, exitCode);
414 Runtime.getRuntime().addShutdownHook(shutdownHook);
415 }
416
417 try
418 {
419 exitCode.set(doToolProcessing());
420 }
421 catch (Exception e)
422 {
423 debugException(e);
424 err(getExceptionMessage(e));
425 exitCode.set(ResultCode.LOCAL_ERROR);
426 }
427 finally
428 {
429 if (shutdownHook != null)
430 {
431 Runtime.getRuntime().removeShutdownHook(shutdownHook);
432 }
433 }
434
435 return exitCode.get();
436 }
437
438
439
440 /**
441 * Retrieves a sorted map of subcommands for the provided argument parser,
442 * alphabetized by primary name.
443 *
444 * @param parser The argument parser for which to get the sorted
445 * subcommands.
446 *
447 * @return The sorted map of subcommands.
448 */
449 private static TreeMap<String,SubCommand> getSortedSubCommands(
450 final ArgumentParser parser)
451 {
452 final TreeMap<String,SubCommand> m = new TreeMap<String,SubCommand>();
453 for (final SubCommand sc : parser.getSubCommands())
454 {
455 m.put(sc.getPrimaryName(), sc);
456 }
457 return m;
458 }
459
460
461
462 /**
463 * Writes example usage information for this tool to the standard output
464 * stream.
465 *
466 * @param parser The argument parser used to process the provided set of
467 * command-line arguments.
468 */
469 private void displayExampleUsages(final ArgumentParser parser)
470 {
471 final LinkedHashMap<String[],String> examples;
472 if ((parser != null) && (parser.getSelectedSubCommand() != null))
473 {
474 examples = parser.getSelectedSubCommand().getExampleUsages();
475 }
476 else
477 {
478 examples = getExampleUsages();
479 }
480
481 if ((examples == null) || examples.isEmpty())
482 {
483 return;
484 }
485
486 out(INFO_CL_TOOL_LABEL_EXAMPLES);
487
488 final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
489 for (final Map.Entry<String[],String> e : examples.entrySet())
490 {
491 out();
492 wrapOut(2, wrapWidth, e.getValue());
493 out();
494
495 final StringBuilder buffer = new StringBuilder();
496 buffer.append(" ");
497 buffer.append(getToolName());
498
499 final String[] args = e.getKey();
500 for (int i=0; i < args.length; i++)
501 {
502 buffer.append(' ');
503
504 // If the argument has a value, then make sure to keep it on the same
505 // line as the argument name. This may introduce false positives due to
506 // unnamed trailing arguments, but the worst that will happen that case
507 // is that the output may be wrapped earlier than necessary one time.
508 String arg = args[i];
509 if (arg.startsWith("-"))
510 {
511 if ((i < (args.length - 1)) && (! args[i+1].startsWith("-")))
512 {
513 ExampleCommandLineArgument cleanArg =
514 ExampleCommandLineArgument.getCleanArgument(args[i+1]);
515 arg += ' ' + cleanArg.getLocalForm();
516 i++;
517 }
518 }
519 else
520 {
521 ExampleCommandLineArgument cleanArg =
522 ExampleCommandLineArgument.getCleanArgument(arg);
523 arg = cleanArg.getLocalForm();
524 }
525
526 if ((buffer.length() + arg.length() + 2) < wrapWidth)
527 {
528 buffer.append(arg);
529 }
530 else
531 {
532 buffer.append('\\');
533 out(buffer.toString());
534 buffer.setLength(0);
535 buffer.append(" ");
536 buffer.append(arg);
537 }
538 }
539
540 out(buffer.toString());
541 }
542 }
543
544
545
546 /**
547 * Retrieves the name of this tool. It should be the name of the command used
548 * to invoke this tool.
549 *
550 * @return The name for this tool.
551 */
552 public abstract String getToolName();
553
554
555
556 /**
557 * Retrieves a human-readable description for this tool.
558 *
559 * @return A human-readable description for this tool.
560 */
561 public abstract String getToolDescription();
562
563
564
565 /**
566 * Retrieves a version string for this tool, if available.
567 *
568 * @return A version string for this tool, or {@code null} if none is
569 * available.
570 */
571 public String getToolVersion()
572 {
573 return null;
574 }
575
576
577
578 /**
579 * Retrieves the minimum number of unnamed trailing arguments that must be
580 * provided for this tool. If a tool requires the use of trailing arguments,
581 * then it must override this method and the {@link #getMaxTrailingArguments}
582 * arguments to return nonzero values, and it must also override the
583 * {@link #getTrailingArgumentsPlaceholder} method to return a
584 * non-{@code null} value.
585 *
586 * @return The minimum number of unnamed trailing arguments that may be
587 * provided for this tool. A value of zero indicates that the tool
588 * may be invoked without any trailing arguments.
589 */
590 public int getMinTrailingArguments()
591 {
592 return 0;
593 }
594
595
596
597 /**
598 * Retrieves the maximum number of unnamed trailing arguments that may be
599 * provided for this tool. If a tool supports trailing arguments, then it
600 * must override this method to return a nonzero value, and must also override
601 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to
602 * return a non-{@code null} value.
603 *
604 * @return The maximum number of unnamed trailing arguments that may be
605 * provided for this tool. A value of zero indicates that trailing
606 * arguments are not allowed. A negative value indicates that there
607 * should be no limit on the number of trailing arguments.
608 */
609 public int getMaxTrailingArguments()
610 {
611 return 0;
612 }
613
614
615
616 /**
617 * Retrieves a placeholder string that should be used for trailing arguments
618 * in the usage information for this tool.
619 *
620 * @return A placeholder string that should be used for trailing arguments in
621 * the usage information for this tool, or {@code null} if trailing
622 * arguments are not supported.
623 */
624 public String getTrailingArgumentsPlaceholder()
625 {
626 return null;
627 }
628
629
630
631 /**
632 * Indicates whether this tool should provide support for an interactive mode,
633 * in which the tool offers a mode in which the arguments can be provided in
634 * a text-driven menu rather than requiring them to be given on the command
635 * line. If interactive mode is supported, it may be invoked using the
636 * "--interactive" argument. Alternately, if interactive mode is supported
637 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
638 * interactive mode may be invoked by simply launching the tool without any
639 * arguments.
640 *
641 * @return {@code true} if this tool supports interactive mode, or
642 * {@code false} if not.
643 */
644 public boolean supportsInteractiveMode()
645 {
646 return false;
647 }
648
649
650
651 /**
652 * Indicates whether this tool defaults to launching in interactive mode if
653 * the tool is invoked without any command-line arguments. This will only be
654 * used if {@link #supportsInteractiveMode()} returns {@code true}.
655 *
656 * @return {@code true} if this tool defaults to using interactive mode if
657 * launched without any command-line arguments, or {@code false} if
658 * not.
659 */
660 public boolean defaultsToInteractiveMode()
661 {
662 return false;
663 }
664
665
666
667 /**
668 * Indicates whether this tool supports the use of a properties file for
669 * specifying default values for arguments that aren't specified on the
670 * command line.
671 *
672 * @return {@code true} if this tool supports the use of a properties file
673 * for specifying default values for arguments that aren't specified
674 * on the command line, or {@code false} if not.
675 */
676 public boolean supportsPropertiesFile()
677 {
678 return false;
679 }
680
681
682
683 /**
684 * Indicates whether this tool should provide arguments for redirecting output
685 * to a file. If this method returns {@code true}, then the tool will offer
686 * an "--outputFile" argument that will specify the path to a file to which
687 * all standard output and standard error content will be written, and it will
688 * also offer a "--teeToStandardOut" argument that can only be used if the
689 * "--outputFile" argument is present and will cause all output to be written
690 * to both the specified output file and to standard output.
691 *
692 * @return {@code true} if this tool should provide arguments for redirecting
693 * output to a file, or {@code false} if not.
694 */
695 protected boolean supportsOutputFile()
696 {
697 return false;
698 }
699
700
701
702 /**
703 * Creates a parser that can be used to to parse arguments accepted by
704 * this tool.
705 *
706 * @return ArgumentParser that can be used to parse arguments for this
707 * tool.
708 *
709 * @throws ArgumentException If there was a problem initializing the
710 * parser for this tool.
711 */
712 public final ArgumentParser createArgumentParser()
713 throws ArgumentException
714 {
715 final ArgumentParser parser = new ArgumentParser(getToolName(),
716 getToolDescription(), getMinTrailingArguments(),
717 getMaxTrailingArguments(), getTrailingArgumentsPlaceholder());
718
719 addToolArguments(parser);
720
721 if (supportsInteractiveMode())
722 {
723 interactiveArgument = new BooleanArgument(null, "interactive",
724 INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get());
725 interactiveArgument.setUsageArgument(true);
726 parser.addArgument(interactiveArgument);
727 }
728
729 if (supportsOutputFile())
730 {
731 outputFileArgument = new FileArgument(null, "outputFile", false, 1, null,
732 INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
733 false);
734 outputFileArgument.addLongIdentifier("output-file");
735 outputFileArgument.setUsageArgument(true);
736 parser.addArgument(outputFileArgument);
737
738 appendToOutputFileArgument = new BooleanArgument(null,
739 "appendToOutputFile", 1,
740 INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get(
741 outputFileArgument.getIdentifierString()));
742 appendToOutputFileArgument.addLongIdentifier("append-to-output-file");
743 appendToOutputFileArgument.setUsageArgument(true);
744 parser.addArgument(appendToOutputFileArgument);
745
746 teeOutputArgument = new BooleanArgument(null, "teeOutput", 1,
747 INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get(
748 outputFileArgument.getIdentifierString()));
749 teeOutputArgument.addLongIdentifier("tee-output");
750 teeOutputArgument.setUsageArgument(true);
751 parser.addArgument(teeOutputArgument);
752
753 parser.addDependentArgumentSet(appendToOutputFileArgument,
754 outputFileArgument);
755 parser.addDependentArgumentSet(teeOutputArgument,
756 outputFileArgument);
757 }
758
759 helpArgument = new BooleanArgument('H', "help",
760 INFO_CL_TOOL_DESCRIPTION_HELP.get());
761 helpArgument.addShortIdentifier('?');
762 helpArgument.setUsageArgument(true);
763 parser.addArgument(helpArgument);
764
765 if (! parser.getSubCommands().isEmpty())
766 {
767 helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1,
768 INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get());
769 helpSubcommandsArgument.addLongIdentifier("help-subcommands");
770 helpSubcommandsArgument.setUsageArgument(true);
771 parser.addArgument(helpSubcommandsArgument);
772 }
773
774 final String version = getToolVersion();
775 if ((version != null) && (version.length() > 0) &&
776 (parser.getNamedArgument("version") == null))
777 {
778 final Character shortIdentifier;
779 if (parser.getNamedArgument('V') == null)
780 {
781 shortIdentifier = 'V';
782 }
783 else
784 {
785 shortIdentifier = null;
786 }
787
788 versionArgument = new BooleanArgument(shortIdentifier, "version",
789 INFO_CL_TOOL_DESCRIPTION_VERSION.get());
790 versionArgument.setUsageArgument(true);
791 parser.addArgument(versionArgument);
792 }
793
794 if (supportsPropertiesFile())
795 {
796 parser.enablePropertiesFileSupport();
797 }
798
799 return parser;
800 }
801
802
803
804 /**
805 * Specifies the argument that is used to retrieve usage information about
806 * SASL authentication.
807 *
808 * @param helpSASLArgument The argument that is used to retrieve usage
809 * information about SASL authentication.
810 */
811 void setHelpSASLArgument(final BooleanArgument helpSASLArgument)
812 {
813 this.helpSASLArgument = helpSASLArgument;
814 }
815
816
817
818 /**
819 * Retrieves a set containing the long identifiers used for usage arguments
820 * injected by this class.
821 *
822 * @param tool The tool to use to help make the determination.
823 *
824 * @return A set containing the long identifiers used for usage arguments
825 * injected by this class.
826 */
827 static Set<String> getUsageArgumentIdentifiers(final CommandLineTool tool)
828 {
829 final LinkedHashSet<String> ids = new LinkedHashSet<String>(9);
830
831 ids.add("help");
832 ids.add("version");
833 ids.add("helpSubcommands");
834
835 if (tool.supportsInteractiveMode())
836 {
837 ids.add("interactive");
838 }
839
840 if (tool.supportsPropertiesFile())
841 {
842 ids.add("propertiesFilePath");
843 ids.add("generatePropertiesFile");
844 ids.add("noPropertiesFile");
845 }
846
847 if (tool.supportsOutputFile())
848 {
849 ids.add("outputFile");
850 ids.add("appendToOutputFile");
851 ids.add("teeOutput");
852 }
853
854 return Collections.unmodifiableSet(ids);
855 }
856
857
858
859 /**
860 * Adds the command-line arguments supported for use with this tool to the
861 * provided argument parser. The tool may need to retain references to the
862 * arguments (and/or the argument parser, if trailing arguments are allowed)
863 * to it in order to obtain their values for use in later processing.
864 *
865 * @param parser The argument parser to which the arguments are to be added.
866 *
867 * @throws ArgumentException If a problem occurs while adding any of the
868 * tool-specific arguments to the provided
869 * argument parser.
870 */
871 public abstract void addToolArguments(final ArgumentParser parser)
872 throws ArgumentException;
873
874
875
876 /**
877 * Performs any necessary processing that should be done to ensure that the
878 * provided set of command-line arguments were valid. This method will be
879 * called after the basic argument parsing has been performed and immediately
880 * before the {@link CommandLineTool#doToolProcessing} method is invoked.
881 * Note that if the tool supports interactive mode, then this method may be
882 * invoked multiple times to allow the user to interactively fix validation
883 * errors.
884 *
885 * @throws ArgumentException If there was a problem with the command-line
886 * arguments provided to this program.
887 */
888 public void doExtendedArgumentValidation()
889 throws ArgumentException
890 {
891 // No processing will be performed by default.
892 }
893
894
895
896 /**
897 * Performs the core set of processing for this tool.
898 *
899 * @return A result code that indicates whether the processing completed
900 * successfully.
901 */
902 public abstract ResultCode doToolProcessing();
903
904
905
906 /**
907 * Indicates whether this tool should register a shutdown hook with the JVM.
908 * Shutdown hooks allow for a best-effort attempt to perform a specified set
909 * of processing when the JVM is shutting down under various conditions,
910 * including:
911 * <UL>
912 * <LI>When all non-daemon threads have stopped running (i.e., the tool has
913 * completed processing).</LI>
914 * <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI>
915 * <LI>When the JVM receives an external kill signal (e.g., via the use of
916 * the kill tool or interrupting the JVM with Ctrl+C).</LI>
917 * </UL>
918 * Shutdown hooks may not be invoked if the process is forcefully killed
919 * (e.g., using "kill -9", or the {@code System.halt()} or
920 * {@code Runtime.halt()} methods).
921 * <BR><BR>
922 * If this method is overridden to return {@code true}, then the
923 * {@link #doShutdownHookProcessing(ResultCode)} method should also be
924 * overridden to contain the logic that will be invoked when the JVM is
925 * shutting down in a manner that calls shutdown hooks.
926 *
927 * @return {@code true} if this tool should register a shutdown hook, or
928 * {@code false} if not.
929 */
930 protected boolean registerShutdownHook()
931 {
932 return false;
933 }
934
935
936
937 /**
938 * Performs any processing that may be needed when the JVM is shutting down,
939 * whether because tool processing has completed or because it has been
940 * interrupted (e.g., by a kill or break signal).
941 * <BR><BR>
942 * Note that because shutdown hooks run at a delicate time in the life of the
943 * JVM, they should complete quickly and minimize access to external
944 * resources. See the documentation for the
945 * {@code java.lang.Runtime.addShutdownHook} method for recommendations and
946 * restrictions about writing shutdown hooks.
947 *
948 * @param resultCode The result code returned by the tool. It may be
949 * {@code null} if the tool was interrupted before it
950 * completed processing.
951 */
952 protected void doShutdownHookProcessing(final ResultCode resultCode)
953 {
954 throw new LDAPSDKUsageException(
955 ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get(
956 getToolName()));
957 }
958
959
960
961 /**
962 * Retrieves a set of information that may be used to generate example usage
963 * information. Each element in the returned map should consist of a map
964 * between an example set of arguments and a string that describes the
965 * behavior of the tool when invoked with that set of arguments.
966 *
967 * @return A set of information that may be used to generate example usage
968 * information. It may be {@code null} or empty if no example usage
969 * information is available.
970 */
971 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
972 public LinkedHashMap<String[],String> getExampleUsages()
973 {
974 return null;
975 }
976
977
978
979 /**
980 * Retrieves the print stream that will be used for standard output.
981 *
982 * @return The print stream that will be used for standard output.
983 */
984 public final PrintStream getOut()
985 {
986 return out;
987 }
988
989
990
991 /**
992 * Retrieves the print stream that may be used to write to the original
993 * standard output. This may be different from the current standard output
994 * stream if an output file has been configured.
995 *
996 * @return The print stream that may be used to write to the original
997 * standard output.
998 */
999 public final PrintStream getOriginalOut()
1000 {
1001 return originalOut;
1002 }
1003
1004
1005
1006 /**
1007 * Writes the provided message to the standard output stream for this tool.
1008 * <BR><BR>
1009 * This method is completely threadsafe and my be invoked concurrently by any
1010 * number of threads.
1011 *
1012 * @param msg The message components that will be written to the standard
1013 * output stream. They will be concatenated together on the same
1014 * line, and that line will be followed by an end-of-line
1015 * sequence.
1016 */
1017 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1018 public final synchronized void out(final Object... msg)
1019 {
1020 write(out, 0, 0, msg);
1021 }
1022
1023
1024
1025 /**
1026 * Writes the provided message to the standard output stream for this tool,
1027 * optionally wrapping and/or indenting the text in the process.
1028 * <BR><BR>
1029 * This method is completely threadsafe and my be invoked concurrently by any
1030 * number of threads.
1031 *
1032 * @param indent The number of spaces each line should be indented. A
1033 * value less than or equal to zero indicates that no
1034 * indent should be used.
1035 * @param wrapColumn The column at which to wrap long lines. A value less
1036 * than or equal to two indicates that no wrapping should
1037 * be performed. If both an indent and a wrap column are
1038 * to be used, then the wrap column must be greater than
1039 * the indent.
1040 * @param msg The message components that will be written to the
1041 * standard output stream. They will be concatenated
1042 * together on the same line, and that line will be
1043 * followed by an end-of-line sequence.
1044 */
1045 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1046 public final synchronized void wrapOut(final int indent, final int wrapColumn,
1047 final Object... msg)
1048 {
1049 write(out, indent, wrapColumn, msg);
1050 }
1051
1052
1053
1054 /**
1055 * Writes the provided message to the standard output stream for this tool,
1056 * optionally wrapping and/or indenting the text in the process.
1057 * <BR><BR>
1058 * This method is completely threadsafe and my be invoked concurrently by any
1059 * number of threads.
1060 *
1061 * @param firstLineIndent The number of spaces the first line should be
1062 * indented. A value less than or equal to zero
1063 * indicates that no indent should be used.
1064 * @param subsequentLineIndent The number of spaces each line except the
1065 * first should be indented. A value less than
1066 * or equal to zero indicates that no indent
1067 * should be used.
1068 * @param wrapColumn The column at which to wrap long lines. A
1069 * value less than or equal to two indicates
1070 * that no wrapping should be performed. If
1071 * both an indent and a wrap column are to be
1072 * used, then the wrap column must be greater
1073 * than the indent.
1074 * @param endWithNewline Indicates whether a newline sequence should
1075 * follow the last line that is printed.
1076 * @param msg The message components that will be written
1077 * to the standard output stream. They will be
1078 * concatenated together on the same line, and
1079 * that line will be followed by an end-of-line
1080 * sequence.
1081 */
1082 final synchronized void wrapStandardOut(final int firstLineIndent,
1083 final int subsequentLineIndent,
1084 final int wrapColumn,
1085 final boolean endWithNewline,
1086 final Object... msg)
1087 {
1088 write(out, firstLineIndent, subsequentLineIndent, wrapColumn,
1089 endWithNewline, msg);
1090 }
1091
1092
1093
1094 /**
1095 * Retrieves the print stream that will be used for standard error.
1096 *
1097 * @return The print stream that will be used for standard error.
1098 */
1099 public final PrintStream getErr()
1100 {
1101 return err;
1102 }
1103
1104
1105
1106 /**
1107 * Retrieves the print stream that may be used to write to the original
1108 * standard error. This may be different from the current standard error
1109 * stream if an output file has been configured.
1110 *
1111 * @return The print stream that may be used to write to the original
1112 * standard error.
1113 */
1114 public final PrintStream getOriginalErr()
1115 {
1116 return originalErr;
1117 }
1118
1119
1120
1121 /**
1122 * Writes the provided message to the standard error stream for this tool.
1123 * <BR><BR>
1124 * This method is completely threadsafe and my be invoked concurrently by any
1125 * number of threads.
1126 *
1127 * @param msg The message components that will be written to the standard
1128 * error stream. They will be concatenated together on the same
1129 * line, and that line will be followed by an end-of-line
1130 * sequence.
1131 */
1132 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1133 public final synchronized void err(final Object... msg)
1134 {
1135 write(err, 0, 0, msg);
1136 }
1137
1138
1139
1140 /**
1141 * Writes the provided message to the standard error stream for this tool,
1142 * optionally wrapping and/or indenting the text in the process.
1143 * <BR><BR>
1144 * This method is completely threadsafe and my be invoked concurrently by any
1145 * number of threads.
1146 *
1147 * @param indent The number of spaces each line should be indented. A
1148 * value less than or equal to zero indicates that no
1149 * indent should be used.
1150 * @param wrapColumn The column at which to wrap long lines. A value less
1151 * than or equal to two indicates that no wrapping should
1152 * be performed. If both an indent and a wrap column are
1153 * to be used, then the wrap column must be greater than
1154 * the indent.
1155 * @param msg The message components that will be written to the
1156 * standard output stream. They will be concatenated
1157 * together on the same line, and that line will be
1158 * followed by an end-of-line sequence.
1159 */
1160 @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1161 public final synchronized void wrapErr(final int indent, final int wrapColumn,
1162 final Object... msg)
1163 {
1164 write(err, indent, wrapColumn, msg);
1165 }
1166
1167
1168
1169 /**
1170 * Writes the provided message to the given print stream, optionally wrapping
1171 * and/or indenting the text in the process.
1172 *
1173 * @param stream The stream to which the message should be written.
1174 * @param indent The number of spaces each line should be indented. A
1175 * value less than or equal to zero indicates that no
1176 * indent should be used.
1177 * @param wrapColumn The column at which to wrap long lines. A value less
1178 * than or equal to two indicates that no wrapping should
1179 * be performed. If both an indent and a wrap column are
1180 * to be used, then the wrap column must be greater than
1181 * the indent.
1182 * @param msg The message components that will be written to the
1183 * standard output stream. They will be concatenated
1184 * together on the same line, and that line will be
1185 * followed by an end-of-line sequence.
1186 */
1187 private static void write(final PrintStream stream, final int indent,
1188 final int wrapColumn, final Object... msg)
1189 {
1190 write(stream, indent, indent, wrapColumn, true, msg);
1191 }
1192
1193
1194
1195 /**
1196 * Writes the provided message to the given print stream, optionally wrapping
1197 * and/or indenting the text in the process.
1198 *
1199 * @param stream The stream to which the message should be
1200 * written.
1201 * @param firstLineIndent The number of spaces the first line should be
1202 * indented. A value less than or equal to zero
1203 * indicates that no indent should be used.
1204 * @param subsequentLineIndent The number of spaces all lines after the
1205 * first should be indented. A value less than
1206 * or equal to zero indicates that no indent
1207 * should be used.
1208 * @param wrapColumn The column at which to wrap long lines. A
1209 * value less than or equal to two indicates
1210 * that no wrapping should be performed. If
1211 * both an indent and a wrap column are to be
1212 * used, then the wrap column must be greater
1213 * than the indent.
1214 * @param endWithNewline Indicates whether a newline sequence should
1215 * follow the last line that is printed.
1216 * @param msg The message components that will be written
1217 * to the standard output stream. They will be
1218 * concatenated together on the same line, and
1219 * that line will be followed by an end-of-line
1220 * sequence.
1221 */
1222 private static void write(final PrintStream stream, final int firstLineIndent,
1223 final int subsequentLineIndent,
1224 final int wrapColumn,
1225 final boolean endWithNewline, final Object... msg)
1226 {
1227 final StringBuilder buffer = new StringBuilder();
1228 for (final Object o : msg)
1229 {
1230 buffer.append(o);
1231 }
1232
1233 if (wrapColumn > 2)
1234 {
1235 boolean firstLine = true;
1236 for (final String line :
1237 wrapLine(buffer.toString(), (wrapColumn - firstLineIndent),
1238 (wrapColumn - subsequentLineIndent)))
1239 {
1240 final int indent;
1241 if (firstLine)
1242 {
1243 indent = firstLineIndent;
1244 firstLine = false;
1245 }
1246 else
1247 {
1248 stream.println();
1249 indent = subsequentLineIndent;
1250 }
1251
1252 if (indent > 0)
1253 {
1254 for (int i=0; i < indent; i++)
1255 {
1256 stream.print(' ');
1257 }
1258 }
1259 stream.print(line);
1260 }
1261 }
1262 else
1263 {
1264 if (firstLineIndent > 0)
1265 {
1266 for (int i=0; i < firstLineIndent; i++)
1267 {
1268 stream.print(' ');
1269 }
1270 }
1271 stream.print(buffer.toString());
1272 }
1273
1274 if (endWithNewline)
1275 {
1276 stream.println();
1277 }
1278 stream.flush();
1279 }
1280 }