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 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 a collection of {@link Option}s 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        if (Util.isEmpty(cmdLineSyntax)) {
299            throw new IllegalArgumentException("cmdLineSyntax not provided");
300        }
301        if (autoUsage) {
302            helpAppendable.appendParagraphFormat("%s %s %s", syntaxPrefix, cmdLineSyntax, toSyntaxOptions(options));
303        } else {
304            helpAppendable.appendParagraphFormat("%s %s", syntaxPrefix, cmdLineSyntax);
305        }
306        if (!Util.isEmpty(header)) {
307            helpAppendable.appendParagraph(header);
308        }
309        helpAppendable.appendTable(getTableDefinition(options));
310        if (!Util.isEmpty(footer)) {
311            helpAppendable.appendParagraph(footer);
312        }
313    }
314
315    /**
316     * Prints the help for {@link Options} with the specified command line syntax.
317     *
318     * @param cmdLineSyntax the syntax for this application
319     * @param header        the banner to display at the beginning of the help
320     * @param options       the {@link Options} to print
321     * @param footer        the banner to display at the end of the help
322     * @param autoUsage     whether to print an automatically generated usage statement
323     * @throws IOException If the output could not be written to the {@link HelpAppendable}
324     */
325    public final void printHelp(final String cmdLineSyntax, final String header, final Options options, final String footer, final boolean autoUsage)
326            throws IOException {
327        printHelp(cmdLineSyntax, header, options.getOptions(), footer, autoUsage);
328    }
329
330    /**
331     * Prints the option table for a collection of {@link Option} objects to the {@link HelpAppendable}.
332     *
333     * @param options the collection of Option objects to print in the table.
334     * @throws IOException If the output could not be written to the {@link HelpAppendable}
335     */
336    public final void printOptions(final Iterable<Option> options) throws IOException {
337        printOptions(getTableDefinition(options));
338    }
339
340    /**
341     * Prints the option table for the specified {@link Options} to the {@link HelpAppendable}.
342     *
343     * @param options the Options to print in the table.
344     * @throws IOException If the output could not be written to the {@link HelpAppendable}
345     */
346    public final void printOptions(final Options options) throws IOException {
347        printOptions(options.getOptions());
348    }
349
350    /**
351     * Prints a {@link TableDefinition} to the {@link HelpAppendable}.
352     *
353     * @param tableDefinition the {@link TableDefinition} to print.
354     * @throws IOException If the output could not be written to the {@link HelpAppendable}
355     */
356    public final void printOptions(final TableDefinition tableDefinition) throws IOException {
357        helpAppendable.appendTable(tableDefinition);
358    }
359
360    /**
361     * Sets the syntax prefix. This is the phrase that is printed before the syntax line.
362     *
363     * @param prefix the new value for the syntax prefix.
364     */
365    public final void setSyntaxPrefix(final String prefix) {
366        this.syntaxPrefix = prefix;
367    }
368
369    /**
370     * Creates a new list of options ordered by the comparator.
371     *
372     * @param options the Options to sort.
373     * @return a new list of options ordered by the comparator.
374     */
375    public List<Option> sort(final Iterable<Option> options) {
376        final List<Option> result = new ArrayList<>();
377        if (options != null) {
378            options.forEach(result::add);
379            result.sort(comparator);
380        }
381        return result;
382    }
383
384    /**
385     * Creates a new list of options ordered by the comparator.
386     *
387     * @param options the Options to sort.
388     * @return a new list of options ordered by the comparator.
389     */
390    public List<Option> sort(final Options options) {
391        return sort(options == null ? null : options.getOptions());
392    }
393
394    /**
395     * Formats the {@code argName} as an argument a defined in the enclosed {@link OptionFormatter.Builder}
396     *
397     * @param argName the string to format as an argument.
398     * @return the {@code argName} formatted as an argument.
399     */
400    public final String toArgName(final String argName) {
401        return optionFormatBuilder.toArgName(argName);
402    }
403
404    /**
405     * Return the string representation of the options as used in the syntax display.
406     *
407     * @param options The collection of {@link Option} instances to create the string representation for.
408     * @return the string representation of the options as used in the syntax display.
409     */
410    public String toSyntaxOptions(final Iterable<Option> options) {
411        return toSyntaxOptions(options, o -> null);
412    }
413
414    /**
415     * Return the string representation of the options as used in the syntax display.
416     *
417     * @param options The options to create the string representation for.
418     * @param lookup  a function to determine if the Option is part of an OptionGroup that has already been processed.
419     * @return the string representation of the options as used in the syntax display.
420     */
421    protected String toSyntaxOptions(final Iterable<Option> options, final Function<Option, OptionGroup> lookup) {
422        // list of groups that have been processed.
423        final Collection<OptionGroup> processedGroups = new ArrayList<>();
424        final List<Option> optList = sort(options);
425        final StringBuilder buff = new StringBuilder();
426        String prefix = "";
427        // iterate over the options
428        for (final Option option : optList) {
429            // get the next Option
430            // check if the option is part of an OptionGroup
431            final OptionGroup optionGroup = lookup.apply(option);
432            // if the option is part of a group
433            if (optionGroup != null) {
434                // and if the group has not already been processed
435                if (!processedGroups.contains(optionGroup)) {
436                    // add the group to the processed list
437                    processedGroups.add(optionGroup);
438                    // add the usage clause
439                    buff.append(prefix).append(toSyntaxOptions(optionGroup));
440                    prefix = " ";
441                }
442                // otherwise the option was displayed in the group previously so ignore it.
443            }
444            // if the Option is not part of an OptionGroup
445            else {
446                buff.append(prefix).append(optionFormatBuilder.build(option).toSyntaxOption());
447                prefix = " ";
448            }
449        }
450        return buff.toString();
451    }
452
453    /**
454     * Return the string representation of the options as used in the syntax display.
455     *
456     * @param group The OptionGroup to create the string representation for.
457     * @return the string representation of the options as used in the syntax display.
458     */
459    public String toSyntaxOptions(final OptionGroup group) {
460        final StringBuilder buff = new StringBuilder();
461        final List<Option> optList = sort(group.getOptions());
462        OptionFormatter formatter = null;
463        // for each option in the OptionGroup
464        final Iterator<Option> iter = optList.iterator();
465        while (iter.hasNext()) {
466            formatter = optionFormatBuilder.build(iter.next());
467            // whether the option is required or not is handled at group level
468            buff.append(formatter.toSyntaxOption(true));
469
470            if (iter.hasNext()) {
471                buff.append(optionGroupSeparator);
472            }
473        }
474        if (formatter != null) {
475            return group.isRequired() ? buff.toString() : formatter.toOptional(buff.toString());
476        }
477        return ""; // there were no entries in the group.
478    }
479
480    /**
481     * Return the string representation of the options as used in the syntax display.
482     *
483     * @param options The {@link Options} to create the string representation for.
484     * @return the string representation of the options as used in the syntax display.
485     */
486    public String toSyntaxOptions(final Options options) {
487        return toSyntaxOptions(options.getOptions(), options::getOptionGroup);
488    }
489}