001/*
002  Licensed to the Apache Software Foundation (ASF) under one or more
003  contributor license agreements.  See the NOTICE file distributed with
004  this work for additional information regarding copyright ownership.
005  The ASF licenses this file to You under the Apache License, Version 2.0
006  (the "License"); you may not use this file except in compliance with
007  the License.  You may obtain a copy of the License at
008
009      https://www.apache.org/licenses/LICENSE-2.0
010
011  Unless required by applicable law or agreed to in writing, software
012  distributed under the License is distributed on an "AS IS" BASIS,
013  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014  See the License for the specific language governing permissions and
015  limitations under the License.
016 */
017package org.apache.commons.cli.help;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Comparator;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Objects;
026import java.util.function.Function;
027import java.util.function.Supplier;
028
029import org.apache.commons.cli.Option;
030import org.apache.commons.cli.OptionGroup;
031import org.apache.commons.cli.Options;
032
033/**
034 * Helps formatters provides the framework to link a {@link HelpAppendable} with a {@link OptionFormatter} and a default {@link TableDefinition} so to produce
035 * standardized format help output.
036 *
037 * @since 1.10.0
038 */
039public abstract class AbstractHelpFormatter {
040
041    /**
042     * Abstracts building instances for subclasses.
043     * <ul>
044     * <li>helpAppendable = a {@link TextHelpAppendable} writing to {@code System.out}</li>
045     * <li>optionFormatter.Builder = the default {@link OptionFormatter.Builder}</li>
046     * </ul>
047     *
048     * @param <B> The builder type.
049     * @param <T> The type to build.
050     */
051    public abstract static class Builder<B extends Builder<B, T>, T extends AbstractHelpFormatter> implements Supplier<T> {
052
053        /** The comparator to sort lists of options */
054        private Comparator<Option> comparator = DEFAULT_COMPARATOR;
055
056        /** The {@link HelpAppendable}. */
057        private HelpAppendable helpAppendable = TextHelpAppendable.systemOut();
058
059        /** The {@link OptionFormatter.Builder} to use to format options in the table. */
060        private OptionFormatter.Builder optionFormatBuilder = OptionFormatter.builder();
061
062        /** The string to separate option groups. */
063        private String optionGroupSeparator = DEFAULT_OPTION_GROUP_SEPARATOR;
064
065        /**
066         * Constructs a new instance.
067         * <p>
068         * Sets {@code showSince} to {@code true}.
069         * </p>
070         */
071        protected Builder() {
072            // empty
073        }
074
075        /**
076         * Returns this instance cast to {@code B}.
077         *
078         * @return {@code this} instance cast to {@code B}.
079         */
080        @SuppressWarnings("unchecked")
081        protected B asThis() {
082            return (B) this;
083        }
084
085        /**
086         * Gets the comparator to sort lists of options.
087         *
088         * @return the comparator to sort lists of options.
089         */
090        protected Comparator<Option> getComparator() {
091            return comparator;
092        }
093
094        /**
095         * Gets {@link HelpAppendable}.
096         *
097         * @return the {@link HelpAppendable}.
098         */
099        protected HelpAppendable getHelpAppendable() {
100            return helpAppendable;
101        }
102
103        /**
104         * Gets {@link OptionFormatter.Builder} to use to format options in the table.
105         *
106         * @return the {@link OptionFormatter.Builder} to use to format options in the table.
107         */
108        protected OptionFormatter.Builder getOptionFormatBuilder() {
109            return optionFormatBuilder;
110        }
111
112        /**
113         * Gets string to separate option groups.
114         *
115         * @return the string to separate option groups.
116         */
117        protected String getOptionGroupSeparator() {
118            return optionGroupSeparator;
119        }
120
121        /**
122         * Sets the comparator to use for sorting options. If set to {@code null} no sorting is performed.
123         *
124         * @param comparator The comparator to use for sorting options.
125         * @return this
126         */
127        public B setComparator(final Comparator<Option> comparator) {
128            this.comparator = comparator;
129            return asThis();
130        }
131
132        /**
133         * Sets the {@link HelpAppendable}.
134         *
135         * @param helpAppendable the {@link HelpAppendable} to use.
136         * @return this
137         */
138        public B setHelpAppendable(final HelpAppendable helpAppendable) {
139            this.helpAppendable = helpAppendable != null ? helpAppendable : TextHelpAppendable.systemOut();
140            return asThis();
141        }
142
143        /**
144         * Sets the {@link OptionFormatter.Builder}.
145         *
146         * @param optionFormatBuilder the {@link OptionFormatter.Builder} to use.
147         * @return this
148         */
149        public B setOptionFormatBuilder(final OptionFormatter.Builder optionFormatBuilder) {
150            this.optionFormatBuilder = optionFormatBuilder != null ? optionFormatBuilder : OptionFormatter.builder();
151            return asThis();
152        }
153
154        /**
155         * Sets the OptionGroup separator. Normally " | " or something similar to denote that only one option may be chosen.
156         *
157         * @param optionGroupSeparator the string to separate option group elements with.
158         * @return this
159         */
160        public B setOptionGroupSeparator(final String optionGroupSeparator) {
161            this.optionGroupSeparator = Util.defaultValue(optionGroupSeparator, "");
162            return asThis();
163        }
164
165    }
166
167    /**
168     * The default comparator for {@link Option} implementations.
169     */
170    public static final Comparator<Option> DEFAULT_COMPARATOR = (opt1, opt2) -> opt1.getKey().compareToIgnoreCase(opt2.getKey());
171
172    /**
173     * The default separator between {@link OptionGroup} elements: {@value}.
174     */
175    public static final String DEFAULT_OPTION_GROUP_SEPARATOR = " | ";
176
177    /**
178     * The string to display at the beginning of the usage statement: {@value}.
179     */
180    public static final String DEFAULT_SYNTAX_PREFIX = "usage: ";
181
182    /** The comparator for sorting {@link Option} collections */
183    private final Comparator<Option> comparator;
184    /**
185     * The {@link HelpAppendable} that produces the final output.
186     */
187    private final HelpAppendable helpAppendable;
188
189    /**
190     * The OptionFormatter.Builder used to display options within the help page.
191     */
192    private final OptionFormatter.Builder optionFormatBuilder;
193
194    /** The separator between {@link OptionGroup} components. */
195    private final String optionGroupSeparator;
196
197    /**
198     * The phrase printed before the syntax line.
199     */
200    private String syntaxPrefix = DEFAULT_SYNTAX_PREFIX;
201
202    /**
203     * Constructs the base formatter.
204     *
205     * @param builder the builder.
206     */
207    protected AbstractHelpFormatter(final Builder<?, ?> builder) {
208        this.helpAppendable = Objects.requireNonNull(builder.getHelpAppendable(), "helpAppendable");
209        this.optionFormatBuilder = Objects.requireNonNull(builder.getOptionFormatBuilder(), "optionFormatBuilder");
210        this.comparator = Objects.requireNonNull(builder.getComparator(), "comparator");
211        this.optionGroupSeparator = Util.defaultValue(builder.getOptionGroupSeparator(), "");
212    }
213
214    /**
215     * Gets the comparator for sorting options.
216     *
217     * @return The comparator for sorting options.
218     */
219    protected Comparator<Option> getComparator() {
220        return comparator;
221    }
222
223    /**
224     * Gets the help appendable.
225     *
226     * @return The help appendable.
227     */
228    protected HelpAppendable getHelpAppendable() {
229        return helpAppendable;
230    }
231
232    /**
233     * Gets the option formatter builder.
234     *
235     * @return The option formatter builder.
236     */
237    protected OptionFormatter.Builder getOptionFormatBuilder() {
238        return optionFormatBuilder;
239    }
240
241    /**
242     * Constructs an {@link OptionFormatter} for the specified {@link Option}.
243     *
244     * @param option The Option to format.
245     * @return an {@link OptionFormatter} for the specified {@link Option}.
246     */
247    public final OptionFormatter getOptionFormatter(final Option option) {
248        return optionFormatBuilder.build(option);
249    }
250
251    /**
252     * Gets the option group separator.
253     *
254     * @return The option group separator.
255     */
256    protected String getOptionGroupSeparator() {
257        return optionGroupSeparator;
258    }
259
260    /**
261     * Gets the {@link HelpAppendable} associated with this help formatter.
262     *
263     * @return The {@link HelpAppendable} associated with this help formatter.
264     */
265    public final HelpAppendable getSerializer() {
266        return helpAppendable;
267    }
268
269    /**
270     * Gets the currently set syntax prefix.
271     *
272     * @return The currently set syntax prefix.
273     */
274    public final String getSyntaxPrefix() {
275        return syntaxPrefix;
276    }
277
278    /**
279     * Converts a collection of {@link Option}s into a {@link TableDefinition}.
280     *
281     * @param options The options to create a table for.
282     * @return the TableDefinition.
283     */
284    protected abstract TableDefinition getTableDefinition(Iterable<Option> options);
285
286    /**
287     * Prints the help for {@link Options} with the specified command line syntax.
288     *
289     * @param cmdLineSyntax the syntax for this application.
290     * @param header        the banner to display at the beginning of the help.
291     * @param options       the collection of {@link Option} objects to print.
292     * @param footer        the banner to display at the end of the help.
293     * @param autoUsage     whether to print an automatically generated usage statement.
294     * @throws IOException If the output could not be written to the {@link HelpAppendable}.
295     */
296    public void printHelp(final String cmdLineSyntax, final String header, final Iterable<Option> options, final String footer, final boolean autoUsage)
297            throws IOException {
298        Options optionsObject = new Options();
299        options.forEach(optionsObject::addOption);
300        printHelp(cmdLineSyntax, header, optionsObject, footer, autoUsage);
301    }
302
303    /**
304     * Prints the help for a collection of {@link Option}s with the specified command line syntax.
305     *
306     * @param cmdLineSyntax the syntax for this application.
307     * @param header        the banner to display at the beginning of the help.
308     * @param options       the collection of {@link Option} objects to print.
309     * @param footer        the banner to display at the end of the help.
310     * @param autoUsage     whether to print an automatically generated usage statement.
311     * @throws IOException If the output could not be written to the {@link HelpAppendable}.
312     */
313    public void printHelp(final String cmdLineSyntax, final String header, final Options options, final String footer, final boolean autoUsage)
314            throws IOException {
315        if (Util.isEmpty(cmdLineSyntax)) {
316            throw new IllegalArgumentException("cmdLineSyntax not provided");
317        }
318        if (autoUsage) {
319            helpAppendable.appendParagraphFormat("%s %s %s", syntaxPrefix, cmdLineSyntax, toSyntaxOptions(options));
320        } else {
321            helpAppendable.appendParagraphFormat("%s %s", syntaxPrefix, cmdLineSyntax);
322        }
323        if (!Util.isEmpty(header)) {
324            helpAppendable.appendParagraph(header);
325        }
326        helpAppendable.appendTable(getTableDefinition(options.getOptions()));
327        if (!Util.isEmpty(footer)) {
328            helpAppendable.appendParagraph(footer);
329        }
330    }
331
332    /**
333     * Prints the option table for a collection of {@link Option} objects to the {@link HelpAppendable}.
334     *
335     * @param options the collection of Option objects to print in the table.
336     * @throws IOException If the output could not be written to the {@link HelpAppendable}.
337     */
338    public final void printOptions(final Iterable<Option> options) throws IOException {
339        printOptions(getTableDefinition(options));
340    }
341
342    /**
343     * Prints the option table for the specified {@link Options} to the {@link HelpAppendable}.
344     *
345     * @param options the Options to print in the table.
346     * @throws IOException If the output could not be written to the {@link HelpAppendable}.
347     */
348    public final void printOptions(final Options options) throws IOException {
349        printOptions(options.getOptions());
350    }
351
352    /**
353     * Prints a {@link TableDefinition} to the {@link HelpAppendable}.
354     *
355     * @param tableDefinition the {@link TableDefinition} to print.
356     * @throws IOException If the output could not be written to the {@link HelpAppendable}.
357     */
358    public final void printOptions(final TableDefinition tableDefinition) throws IOException {
359        helpAppendable.appendTable(tableDefinition);
360    }
361
362    /**
363     * Sets the syntax prefix. This is the phrase that is printed before the syntax line.
364     *
365     * @param prefix the new value for the syntax prefix.
366     */
367    public final void setSyntaxPrefix(final String prefix) {
368        this.syntaxPrefix = prefix;
369    }
370
371    /**
372     * Creates a new list of options ordered by the comparator.
373     *
374     * @param options the Options to sort.
375     * @return a new list of options ordered by the comparator.
376     */
377    public List<Option> sort(final Iterable<Option> options) {
378        final List<Option> result = new ArrayList<>();
379        if (options != null) {
380            options.forEach(result::add);
381            result.sort(comparator);
382        }
383        return result;
384    }
385
386    /**
387     * Creates a new list of options ordered by the comparator.
388     *
389     * @param options the Options to sort.
390     * @return a new list of options ordered by the comparator.
391     */
392    public List<Option> sort(final Options options) {
393        return sort(options == null ? null : options.getOptions());
394    }
395
396    /**
397     * Formats the {@code argName} as an argument a defined in the enclosed {@link OptionFormatter.Builder}.
398     *
399     * @param argName the string to format as an argument.
400     * @return the {@code argName} formatted as an argument.
401     */
402    public final String toArgName(final String argName) {
403        return optionFormatBuilder.toArgName(argName);
404    }
405
406    /**
407     * Return the string representation of the options as used in the syntax display.
408     * <p>
409     *     This is probably not the method you want.  This method does not track the presence
410     *     of option groups.  To display the option grouping use {@link #toSyntaxOptions(Options)} or
411     *     {@link #toSyntaxOptions(OptionGroup)} for individual groups.
412     * </p>
413     * @param options The collection of {@link Option} instances to create the string representation for.
414     * @return the string representation of the options as used in the syntax display.
415     */
416    public String toSyntaxOptions(final Iterable<Option> options) {
417        return toSyntaxOptions(options, o -> null);
418    }
419
420    /**
421     * Return the string representation of the options as used in the syntax display.
422     *
423     * @param options The options to create the string representation for.
424     * @param lookup  a function to determine if the Option is part of an OptionGroup that has already been processed.
425     * @return the string representation of the options as used in the syntax display.
426     */
427    protected String toSyntaxOptions(final Iterable<Option> options, final Function<Option, OptionGroup> lookup) {
428        // list of groups that have been processed.
429        final Collection<OptionGroup> processedGroups = new ArrayList<>();
430        final List<Option> optList = sort(options);
431        final StringBuilder buff = new StringBuilder();
432        String prefix = "";
433        // iterate over the options
434        for (final Option option : optList) {
435            // get the next Option
436            // check if the option is part of an OptionGroup
437            final OptionGroup optionGroup = lookup.apply(option);
438            // if the option is part of a group
439            if (optionGroup != null) {
440                // and if the group has not already been processed
441                if (!processedGroups.contains(optionGroup)) {
442                    // add the group to the processed list
443                    processedGroups.add(optionGroup);
444                    // add the usage clause
445                    buff.append(prefix).append(toSyntaxOptions(optionGroup));
446                    prefix = " ";
447                }
448                // otherwise the option was displayed in the group previously so ignore it.
449            }
450            // if the Option is not part of an OptionGroup
451            else {
452                buff.append(prefix).append(optionFormatBuilder.build(option).toSyntaxOption());
453                prefix = " ";
454            }
455        }
456        return buff.toString();
457    }
458
459    /**
460     * Return the string representation of the options as used in the syntax display.
461     *
462     * @param group The OptionGroup to create the string representation for.
463     * @return the string representation of the options as used in the syntax display.
464     */
465    public String toSyntaxOptions(final OptionGroup group) {
466        final StringBuilder buff = new StringBuilder();
467        final List<Option> optList = sort(group.getOptions());
468        OptionFormatter formatter = null;
469        // for each option in the OptionGroup
470        final Iterator<Option> iter = optList.iterator();
471        while (iter.hasNext()) {
472            formatter = optionFormatBuilder.build(iter.next());
473            // whether the option is required or not is handled at group level
474            buff.append(formatter.toSyntaxOption(true));
475            if (iter.hasNext()) {
476                buff.append(optionGroupSeparator);
477            }
478        }
479        if (formatter != null) {
480            return group.isRequired() ? buff.toString() : formatter.toOptional(buff.toString());
481        }
482        return ""; // there were no entries in the group.
483    }
484
485    /**
486     * Return the string representation of the options as used in the syntax display.
487     *
488     * @param options The {@link Options} to create the string representation for.
489     * @return the string representation of the options as used in the syntax display.
490     */
491    public String toSyntaxOptions(final Options options) {
492        return toSyntaxOptions(options.getOptions(), options::getOptionGroup);
493    }
494}