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