1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Portions Copyright (c) 2017, Steven Haehn.
22  * Portions Copyright (c) 2019, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.util;
25 
26 import java.io.PrintWriter;
27 import java.io.StringWriter;
28 import java.io.BufferedReader;
29 import java.io.IOException;
30 import java.io.InputStreamReader;
31 import java.io.PrintStream;
32 import java.text.DateFormat;
33 import java.text.ParseException;
34 import java.util.List;
35 import java.util.function.Consumer;
36 import java.util.function.Function;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Date;
40 import java.util.HashMap;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 
44 /**
45  * OptionParser is a class for command-line option analysis.
46  *
47  * Now that Java 8 has the crucial Lambda and Consumer interfaces, we can
48  * implement a more powerful program option parsing mechanism, ala ruby
49  * OptionParser style.
50  *
51  * Features
52  *   o  An option can have multiple short names (-x) and multiple long
53  *      names (--xyz). Thus, an option that displays a programs usage
54  *      may be available as -h, -?, --help, --usage, and --about.
55  *
56  *   o  An option may be specified as having no argument, an optional
57  *      argument, or a required argument. Arguments may be validated
58  *      against a regular expression pattern or a list of valid values.
59  *
60  *   o  The argument specification and the code to handle it are
61  *      written in the same place. The argument description may consist
62  *      of one or more lines to be used when displaying usage summary.
63  *
64  *   o  The option summary is produced without maintaining strings
65  *      in a separate setting.
66  *
67  *   o  Users are allowed to enter initial substrings for long option
68  *      names as long as there is no ambiguity.
69  *
70  *   o  Supports the ability to coerce command line arguments into objects.
71  *      This class readily supports Boolean (yes/no,true/false,on/off),
72  *      Float, Double, Integer, and String[] (strings separated by comma)
73  *      objects. The programmer may define additional coercions of their own.
74  *
75  * @author Steven Haehn
76  */
77 public class OptionParser {
78 
79     // Used to hold data type converters
80     @SuppressWarnings("rawtypes")
81     private static HashMap<Class, DataParser> converters = new HashMap<>();
82 
83     @SuppressWarnings("rawtypes")
84     static class DataParser {
85         Class dataType;
86         Function<String, Object> converter;
87 
DataParser(Class cls, Function<String, Object> converter)88         DataParser(Class cls, Function<String, Object> converter) {
89             this.dataType = cls;
90             this.converter = converter;
91         }
92     }
93 
94     // Supported internal data type converters.
95     static {
accept(Integer.class, Integer::parseInt)96         accept(Integer.class, Integer::parseInt);
accept(Boolean.class, OptionParser::parseVerity)97         accept(Boolean.class, OptionParser::parseVerity);
accept(Float.class, Float::parseFloat)98         accept(Float.class, Float::parseFloat);
accept(Double.class, Double::parseDouble)99         accept(Double.class, Double::parseDouble);
accept(String[].class, s -> s.split(R))100         accept(String[].class, s -> s.split(","));
101     }
102 
103     // Option object referenced by its name(s)
104     private final HashMap<String, Option> options;
105 
106     // List of options in order of declaration
107     private final List<Option> optionList;
108 
109     // Keeps track of separator elements placed in option summary
110     private final List<Object> usageSummary;
111 
112     private boolean scanning = false;
113 
114     private String prologue;  // text emitted before option summary
115     private String epilogue;  // text emitted after options summary
116 
117     public class Option {
118 
119         List<String> names;          // option names/aliases
120         String argument;             // argument name for summary
121         String value;                // user entered value for option
122         Class<?> valueType;          // eg. Integer.class, other than String
123         Pattern valuePattern;        // pattern used to accept value
124         List<String> allowedValues;  // list of restricted values
125         Boolean mandatory;           // true/false when argument present
126         StringBuilder description;   // option description for summary
127         Consumer<Object> action;     // code to execute when option encountered
128 
Option()129         public Option() {
130             names = new ArrayList<>();
131         }
132 
addOption(String option, String arg)133         void addOption(String option, String arg) throws IllegalArgumentException {
134             addAlias(option);
135             setArgument(arg);
136         }
137 
addAlias(String alias)138         void addAlias(String alias) throws IllegalArgumentException {
139             names.add(alias);
140 
141             if (options.containsKey(alias)) {
142                 throw new IllegalArgumentException("** Programmer error! Option " + alias + " already defined");
143             }
144 
145             options.put(alias, this);
146         }
147 
setAllowedValues(String[] allowed)148         void setAllowedValues(String[] allowed) {
149             allowedValues = Arrays.asList(allowed);
150         }
151 
setValueType(Class<?> type)152         void setValueType(Class<?> type) {
153             valueType = type;
154         }
155 
setArgument(String arg)156         void setArgument(String arg) {
157             argument = arg.trim();
158             mandatory = !argument.startsWith("[");
159         }
160 
setPattern(String pattern)161         void setPattern(String pattern) {
162             valuePattern = Pattern.compile(pattern);
163         }
164 
addDescription(String descrip)165         void addDescription(String descrip) {
166             if (description == null) {
167                 description = new StringBuilder();
168             }
169             description.append(descrip);
170             description.append("\n");
171         }
172 
173         /**
174          * Code to be activated when option encountered.
175          *
176          * @param action is the code that will be called when the
177          * parser encounters the associated named option in its
178          * argument list.
179          */
Do(Consumer<Object> action)180         public void Do(Consumer<Object> action) {
181             this.action = action;
182         }
183 
getUsage()184         String getUsage() {
185             StringBuilder line = new StringBuilder();
186             String separator = "";
187             for (String name : names) {
188                 line.append(separator);
189                 line.append(name);
190                 separator = ", ";
191             }
192 
193             if (argument != null) {
194                 line.append(' ');
195                 line.append(argument);
196             }
197             line.append("\n");
198             if (description != null) {
199                 line.append("\t");
200                 line.append(description.toString().replaceAll("\\n", "\n\t"));
201             }
202 
203             return line.toString();
204         }
205     }
206 
207     /**
208      * Instantiate a new option parser
209      *
210      * This allows the programmer to create an empty option parser that can
211      * be added to incrementally elsewhere in the program being built. For
212      * example:
213      *
214      *   OptionParser parser = OptionParser();
215      *     .
216      *     .
217      *   parser.prologue = "Usage: program [options] [file [...]]
218      *
219      *   parser.on("-?", "--help", "Display this usage.").Do( v -&gt; {
220      *       parser.help();
221      *   });
222      */
OptionParser()223     public OptionParser() {
224         optionList = new ArrayList<>();
225         options = new HashMap<>();
226         usageSummary = new ArrayList<>();
227     }
228 
229     // Allowable text values for Boolean.class, with case insensitivity.
230     private static final Pattern VERITY = Pattern.compile("(?i)(true|yes|on)");
231     private static final Pattern FALSEHOOD = Pattern.compile("(?i)(false|no|off)");
232 
parseVerity(String text)233     private static Boolean parseVerity(String text) {
234         Matcher m = VERITY.matcher(text);
235         Boolean veracity = false;
236 
237         if (m.matches()) {
238             veracity = true;
239         } else {
240             m = FALSEHOOD.matcher(text);
241             if (m.matches()) {
242                 veracity = false;
243             } else {
244                 throw new IllegalArgumentException();
245             }
246         }
247         return veracity;
248     }
249 
250     /**
251      * Supply parser with data conversion mechanism for option value.
252      * The following is an example usage used internally:
253      *
254      *    accept(Integer.class, s -&gt; { return Integer.parseInt(s); });
255      *
256      * @param type is the internal data class to which an option
257      * value should be converted.
258      *
259      * @param parser is the conversion code that will take the given
260      * option value string and produce the named data type.
261      */
262     @SuppressWarnings("rawtypes")
accept(Class type, Function<String, Object> parser)263     public static void accept(Class type, Function<String, Object> parser) {
264         converters.put(type, new DataParser(type, parser));
265     }
266 
267     /**
268      * Instantiate a new options parser and construct option actionable components.
269      *
270      * As an example:
271      *
272      * <code>
273      *   OptionParser opts = OptionParser.Do(parser -&gt; {
274      *
275      *      parser.prologue =
276      *          String.format("\nUsage: %s [options] [subDir1 [...]]\n", program);
277      *
278      *      parser.on("-?", "--help", "Display this usage.").Do( v -&gt; {
279      *          parser.help();
280      *      });
281      *
282      *      parser.epilogue = "That's all folks!";
283      *   }
284      * </code>
285      *
286      * @param parser consumer
287      * @return OptionParser object
288      */
Do(Consumer<OptionParser> parser)289     public static OptionParser Do(Consumer<OptionParser> parser) {
290         OptionParser me = new OptionParser();
291         parser.accept(me);
292         return me;
293     }
294 
295     /**
296      * Provide a 'scanning' option parser.
297      *
298      * This type of parser only operates on the arguments for which it
299      * is constructed. All other arguments passed to it are ignored.
300      * That is, it won't raise any errors for unrecognizable input as
301      * the normal option parser would.
302      *
303      * @param parser consumer
304      * @return OptionParser object
305      */
scan(Consumer<OptionParser> parser)306     public static OptionParser scan(Consumer<OptionParser> parser) {
307         OptionParser me = new OptionParser();
308         parser.accept(me);
309         me.scanning = true;
310         return me;
311     }
312 
313     /**
314      * Construct option recognition and description object
315      *
316      * This method is used to build the option object which holds
317      * its recognition and validation criteria, description and
318      * ultimately its data handler.
319      *
320      * The 'on' parameters consist of formatted strings which provide the
321      * option names, whether or not the option takes on a value and,
322      * if so, the option value type (mandatory/optional). The data type
323      * of the option value may also be provided to allow the parser to
324      * handle conversion from a string to an internally supported data type.
325      *
326      * Other parameters which may be provided are:
327      *
328      *  o String array of legal option values (eg. {"on","off"})
329      *  o Regular expression pattern that option value must match
330      *  o Multiple line description for the option.
331      *
332      * There are two forms of option names, short and long. The short
333      * option names are a single character in length and are recognized
334      * with a single "-" character to the left of the name (eg. -o).
335      * The long option names hold more than a single character and are
336      * recognized via "--" to the left of the name (eg. --option). The
337      * syntax for specifying an option, whether it takes on a value or
338      * not, and whether that value is mandatory or optional is as follows.
339      * (Note, the use of OPT is an abbreviation for OPTIONAL.)
340      *
341      * Short name 'x':
342      *    -x, -xVALUE, -x=VALUE, -x[OPT], -x[=OPT], -x PLACE
343      *
344      * The option has the short name 'x'. The first form has no value.
345      * the next two require values, the next two indicate that the value
346      * is optional (delineated by the '[' character). The last form
347      * indicates that the option must have a value, but that it follows
348      * the option indicator.
349      *
350      * Long name 'switch':
351      *    --switch, --switch=VALUE, --switch=[OPT], --switch PLACE
352      *
353      * The option has the long name 'switch'. The first form indicates
354      * it does not require a value, The second form indicates that the
355      * option requires a value. The third form indicates that option may
356      * or may not have value. The last form indicates that a value is
357      * required, but that it follows the option indicator.
358      *
359      * Since an option may have multiple names (aliases), there is a
360      * short hand for describing those which take on a value.
361      *
362      * Option value shorthand:  =VALUE, =[OPT]
363      *
364      * The first form indicates that the option value is required, the
365      * second form indicates that the value is optional. For example
366      * the following code says there is an option known by the aliases
367      * -a, -b, and -c and that it needs a required value shown as N.
368      *
369      * <code>
370      *     opt.on( "-a", "-b", "-c", "=N" )
371      * </code>
372      *
373      * When an option takes on a value, 'on' may accept a regular expression
374      * indicating what kind of values are acceptable. The regular expression
375      * is indicated by surrounding the expression with '/' character. For
376      * example, "/pattern/" indicates that the only value acceptable is the
377      * word 'pattern'.
378      *
379      * Any string that does not start with a '-', '=', or '/' is used as a
380      * description for the option in the summary. Multiple descriptions may
381      * be given; they will be shown on additional lines.
382      *
383      * For programmers:  If a switch starts with 3 dashes (---) it will
384      * be hidden from the usage summary and manual generation. It is meant
385      * for unit testing access.
386      *
387      * @param args arguments
388      * @return Option
389      */
on(Object... args)390     public Option on(Object... args) {
391 
392         Option opt = new Option();
393 
394         // Once description starts, then no other option settings are eligible.
395         boolean addedDescription = false;
396 
397         for (Object arg : args) {
398             if (arg instanceof String) {
399                 String argument = (String) arg;
400                 if (addedDescription) {
401                     opt.addDescription(argument);
402                 } else if (argument.startsWith("--")) {
403                     // handle --switch --switch=ARG --switch=[OPT] --switch PLACE
404                     String[] parts = argument.split("[ =]");
405 
406                     if (parts.length == 1) {
407                         opt.addAlias(parts[0]);
408                     } else {
409                         opt.addOption(parts[0], parts[1]);
410                     }
411                 } else if (argument.startsWith("-")) {
412                     // handle -x -xARG -x=ARG -x[OPT] -x[=OPT] -x PLACE
413                     String optName = argument.substring(0, 2);
414                     String remainder = argument.substring(2);
415                     opt.addOption(optName, remainder);
416 
417                 } else if (argument.startsWith("=")) {
418                     opt.setArgument(argument.substring(1));
419                 } else if (argument.startsWith("/")) {
420                     // regular expression (sans '/'s)
421                     opt.setPattern(argument.substring(1, argument.length() - 1));
422                 } else {
423                     // this is description
424                     opt.addDescription(argument);
425                     addedDescription = true;
426                 }
427             // This is indicator for a addOption of specific allowable option values
428             } else if (arg instanceof String[]) {
429                 opt.setAllowedValues((String[]) arg);
430             // This is indicator for option value data type
431             // to which the parser will take and convert.
432             } else if (arg instanceof Class) {
433                 opt.setValueType((Class<?>) arg);
434             } else if (arg == null) {
435                 throw new IllegalArgumentException("arg is null");
436             } else {
437                 throw new IllegalArgumentException("Invalid arg: " +
438                         arg.getClass().getSimpleName() + " " + arg);
439             }
440         }
441 
442         // options starting with 3 dashes are to be hidden from usage.
443         // (the idea here is to hide any unit test entries from general user)
444         if (!opt.names.get(0).startsWith("---")) {
445             optionList.add(opt);
446             usageSummary.add(opt);
447         }
448 
449         return opt;
450     }
451 
argValue(String arg, Boolean mandatory)452     private String argValue(String arg, Boolean mandatory) {
453         // Initially assume that the given argument is going
454         // to be the option's value. Note that if the argument
455         // is actually another option (starts with '-') then
456         // there is no value available. If the option is required
457         // to have a value, null is returned. If the option
458         // does not require a value, an empty string is returned.
459         String value = arg;
460         boolean isOption = value.startsWith("-");
461 
462         if (mandatory) {
463             if (isOption ) {
464                 value = null;
465             }
466         } else if (isOption) {
467             value = "";
468         }
469         return value;
470     }
471 
getOption(String arg, int index)472     private String getOption(String arg, int index) throws ParseException {
473         String option = null;
474 
475         if ( arg.equals("-")) {
476             throw new ParseException("Stand alone '-' found in arguments, not allowed", index);
477         }
478 
479         if (arg.startsWith("-")) {
480             if (arg.startsWith("--")) {
481                 option = arg;                 // long name option (--longOption)
482             } else if (arg.length() > 2) {
483                 option = arg.substring(0, 2); // short name option (-xValue)
484             } else {
485                 option = arg;                 // short name option (-x)
486             }
487         }
488         return option;
489     }
490 
491     /**
492      * Discover full name of partial option name.
493      *
494      * @param option is the initial substring of a long option name.
495      * @param index into original argument list (only used by ParseException)
496      * @return full name of given option substring, or null when not found.
497      * @throws ParseException when more than one candidate name is found.
498      */
candidate(String option, int index)499     protected String candidate(String option, int index) throws ParseException {
500         boolean found = options.containsKey(option);
501         List<String> candidates = new ArrayList<>();
502         String candidate = null;
503 
504         if (found) {
505             candidate = option;
506         } else {
507             // Now check to see if initial substring was entered.
508             for (String key: options.keySet()) {
509                 if (key.startsWith(option)) {
510                     candidates.add(key);
511                 }
512             }
513             if (candidates.size() == 1 ) {
514                 candidate = candidates.get(0);
515             } else if (candidates.size() > 1) {
516                 throw new ParseException(
517                     "Ambiguous option " + option + " matches " + candidates, index);
518             }
519         }
520         return candidate;
521     }
522 
523     /**
524      * Parse given set of arguments and activate handlers
525      *
526      * This code parses the given set of parameters looking for a described
527      * set of options and activates the code segments associated with the
528      * option.
529      *
530      * Parsing is discontinued when a lone "--" is encountered in the list of
531      * arguments. If this is a normal non-scan parser, unrecognized options
532      * will cause a parse exception. If this is a scan parser, unrecognized
533      * options are ignored.
534      *
535      * @param args argument vector
536      * @return non-option parameters, or all arguments after "--" encountered.
537      * @throws ParseException parse exception
538      */
539 
parse(String[] args)540     public String[] parse(String[] args) throws ParseException {
541         int ii = 0;
542         int optind = -1;
543         String option;
544         while (ii < args.length) {
545             option = getOption(args[ii], ii);
546 
547             // When scanning for specific options...
548             if (scanning) {
549                 if (option == null || (option = candidate(option, ii)) == null) {
550                     optind = ++ii;  // skip over everything else
551                     continue;
552                 }
553             }
554 
555             if (option == null) {  // no more options? we be done.
556                 break;
557             } else if (option.equals("--")) {  // parsing escape found? we be done.
558                 optind = ii + 1;
559                 break;
560             } else {
561 
562                 if ( !scanning ) {
563                     String candidate = candidate(option, ii);
564                     if (candidate != null) {
565                         option = candidate;
566                     } else {
567                         throw new ParseException("Unknown option: " + option, ii);
568                     }
569                 }
570                 Option opt = options.get(option);
571                 opt.value = null;
572 
573                 if (option.length() == 2 && !option.equals(args[ii])) {  // catches -xValue
574                     opt.value = args[ii].substring(2);
575                 }
576 
577                 // No argument required?
578                 if (opt.argument == null || opt.argument.equals("")) {
579                     if (opt.value != null) {
580                         throw new ParseException("Option " + option + " does not use value.", ii);
581                     }
582                     opt.value = "";
583 
584                 // Argument specified but value not yet acquired
585                 } else if (opt.value == null) {
586 
587                     ii++;   // next argument may hold argument value
588 
589                     // When option is last in list...
590                     if (ii >= args.length) {
591                         if (!opt.mandatory) {
592                             opt.value = "";  // indicate this option's value was optional
593                         }
594                     } else {
595 
596                         // Look at next argument for value
597                         opt.value = argValue(args[ii], opt.mandatory);
598 
599                         if (opt.value != null && opt.value.equals("")) {
600                             // encountered another option so this
601                             // option's value was not required. Backup
602                             // argument list index to handle so loop
603                             // can re-examine this option.
604                             ii--;
605                         }
606                     }
607                 }
608 
609                 // If there is no value setting for the
610                 // option by now, throw a hissy fit.
611                 if (opt.value == null) {
612                     throw new ParseException("Option " + option + " requires a value.", ii);
613                 }
614 
615                 // Only specific values allowed?
616                 if (opt.allowedValues != null) {
617                     if (!opt.allowedValues.contains(opt.value)) {
618                         throw new ParseException(
619                            "'" + opt.value +
620                            "' is unknown value for option " + opt.names +
621                            ". Must be one of " + opt.allowedValues, ii);
622                     }
623                 }
624 
625                 Object value = opt.value;
626 
627                 // Should option argument match some pattern?
628                 if (opt.valuePattern != null) {
629                     Matcher m = opt.valuePattern.matcher(opt.value);
630                     if (!m.matches()) {
631                         throw new ParseException(
632                            "Value '" + opt.value + "' for option " + opt.names + opt.argument +
633                            "\n does not match pattern " + opt.valuePattern, ii);
634                     }
635 
636                 // Handle special conversions of input
637                 // arguments before sending to action handler.
638                 } else if (opt.valueType != null) {
639 
640                     if (!converters.containsKey(opt.valueType)) {
641                         throw new ParseException(
642                             "No conversion handler for data type " + opt.valueType, ii);
643                     }
644 
645                     try {
646                         DataParser data = converters.get(opt.valueType);
647                         value = data.converter.apply(opt.value);
648 
649                     } catch (Exception e) {
650                         System.err.println("** " + e.getMessage());
651                         throw new ParseException("Failed to parse (" + opt.value + ") as value of " + opt.names, ii);
652                     }
653                 }
654 
655                 if (opt.action != null) {
656                     opt.action.accept(value); // 'do' assigned action
657                 }
658                 optind = ++ii;
659             }
660         }
661 
662         // Prepare to gather any remaining arguments
663         // to send back to calling program.
664 
665         String[] remainingArgs = null;
666 
667         if (optind == -1) {
668             remainingArgs = args;
669         } else if (optind < args.length) {
670             remainingArgs = Arrays.copyOfRange(args, optind, args.length);
671         } else {
672             remainingArgs = new String[0];  // all args used up, send back empty.
673         }
674 
675         return remainingArgs;
676     }
677 
getPrologue()678     private String getPrologue() {
679         // Assign default prologue statement when none given.
680         if (prologue == null) {
681             prologue = "Usage: MyProgram [options]";
682         }
683 
684         return prologue;
685     }
686 
687 
688     /**
689      * Define the prologue to be presented before the options summary.
690      * Example: Usage programName [options]
691      * @param text that makes up the prologue.
692      */
setPrologue(String text)693     public void setPrologue(String text) {
694         prologue = text;
695     }
696 
697     /**
698      * Define the epilogue to be presented after the options summary.
699      * @param text that makes up the epilogue.
700      */
setEpilogue(String text)701     public void setEpilogue(String text) {
702         epilogue = text;
703     }
704 
705     /**
706      * Place text in option summary.
707      * @param text to be inserted into option summary.
708      *
709      * Example usage:
710      * <code>
711      *  OptionParser opts = OptionParser.Do( parser -&gt; {
712      *
713      *    parser.prologue = String.format("Usage: %s [options] bubba smith", program);
714      *    parser.separator("");
715      *
716      *    parser.on("-y value", "--why me", "This is a description").Do( v -&gt; {
717      *        System.out.println("got " + v);
718      *    });
719      *
720      *    parser.separator("  ----------------------------------------------");
721      *    parser.separator("  Common Options:");
722      *    ...
723      *
724      *    parser.separator("  ----------------------------------------------");
725      *    parser.epilogue = "  That's all Folks!";
726      * </code>
727      */
separator(String text)728     public void separator(String text) {
729         usageSummary.add(text);
730     }
731 
732     /**
733      * Obtain option summary.
734      * @param indent a string to be used as the option summary initial indent.
735      * @return usage string
736      */
getUsage(String indent)737     public String getUsage(String indent) {
738 
739         StringWriter wrt = new StringWriter();
740         try (PrintWriter out = new PrintWriter(wrt)) {
741             out.println(getPrologue());
742             for (Object o : usageSummary) {
743                 // Need to be able to handle separator strings
744                 if (o instanceof String) {
745                     out.println((String) o);
746                 } else {
747                     out.println(indent + ((Option) o).getUsage());
748                 }
749             }
750             if (epilogue != null) {
751                 out.println(epilogue);
752             }
753             out.flush();
754         }
755         return wrt.toString();
756     }
757 
758     /**
759      * Obtain option summary.
760      * @return option summary
761      */
getUsage()762     public String getUsage() {
763         return getUsage("  ");
764     }
765 
766     /**
767      * Print out option summary.
768      */
help()769     public void help() {
770         System.out.println(getUsage());
771     }
772 
773     /**
774      * Print out option summary on provided output stream.
775      * @param out print stream
776      */
help(PrintStream out)777     public void help(PrintStream out) {
778         out.println(getUsage());
779     }
780 
spool(BufferedReader reader, PrintWriter out, String tag)781     private void spool(BufferedReader reader, PrintWriter out, String tag) throws IOException {
782         String line;
783         while ((line = reader.readLine()) != null) {
784             if (line.equals(tag)) {
785                 return;
786             }
787             out.println(line);
788         }
789     }
790 
791     /**
792      * Generate XML manual page
793      * This requires the template file <code>opengrok.xml</code> as input.
794      * @return String containing generated XML manual page
795      * @throws IOException I/O exception
796      */
getManPage()797     public String getManPage() throws IOException {
798         StringWriter wrt = new StringWriter();
799         PrintWriter out = new PrintWriter(wrt);
800         try (BufferedReader reader = new BufferedReader(new InputStreamReader(
801                      getClass().getResourceAsStream("/manpage/opengrok.xml"), "US-ASCII"))) {
802             spool(reader, out, "___INSERT_DATE___");
803             out.print("<refmiscinfo class=\"date\">");
804             out.print(DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date()));
805             out.println("</refmiscinfo>");
806 
807             spool(reader, out, "___INSERT_USAGE___");
808             for (Option o: optionList) {
809                 String sep = "";
810                 out.println("<optional><option>");
811                 for (String option : o.names) {
812                     out.print(sep + option);
813                     sep = ", ";
814                 }
815                 if (o.argument != null) {
816                     out.print(" <replaceable>");
817                     out.print(o.argument);
818                     out.print("</replaceable>");
819                 }
820                 out.println("</option></optional>");
821             }
822 
823             spool(reader, out, "___INSERT_OPTIONS___");
824             for (Option o: optionList) {
825                 String sep = "";
826                 out.print("<varlistentry><term><option>");
827                 for (String option : o.names) {
828                     out.print(sep + option);
829                     sep = ", ";
830                 }
831                 out.print("</option></term><listitem><para>");
832                 out.print(o.description);
833                 out.println("</para></listitem></varlistentry>");
834             }
835 
836             spool(reader, out, "___END_OF_FILE___");
837             out.flush();
838         }
839 
840         return wrt.toString();
841     }
842 
getOptionList()843     protected List<Option> getOptionList() {
844         return optionList;
845     }
846 }
847