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 *     http://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.cli2.util;
018
019import java.io.PrintWriter;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Set;
027
028import org.apache.commons.cli2.DisplaySetting;
029import org.apache.commons.cli2.Group;
030import org.apache.commons.cli2.HelpLine;
031import org.apache.commons.cli2.Option;
032import org.apache.commons.cli2.OptionException;
033import org.apache.commons.cli2.resource.ResourceConstants;
034import org.apache.commons.cli2.resource.ResourceHelper;
035
036/**
037 * Presents on screen help based on the application's Options
038 */
039public class HelpFormatter {
040    /**
041     * The default screen width
042     */
043    public static final int DEFAULT_FULL_WIDTH = 80;
044
045    /**
046     * The default screen furniture left of screen
047     */
048    public static final String DEFAULT_GUTTER_LEFT = "";
049
050    /**
051     * The default screen furniture right of screen
052     */
053    public static final String DEFAULT_GUTTER_CENTER = "    ";
054
055    /**
056     * The default screen furniture between columns
057     */
058    public static final String DEFAULT_GUTTER_RIGHT = "";
059
060    /**
061     * The default DisplaySettings used to select the elements to display in the
062     * displayed line of full usage information.
063     *
064     * @see DisplaySetting
065     */
066    public static final Set DEFAULT_FULL_USAGE_SETTINGS;
067
068    /**
069     * The default DisplaySettings used to select the elements of usage per help
070     * line in the main body of help
071     *
072     * @see DisplaySetting
073     */
074    public static final Set DEFAULT_LINE_USAGE_SETTINGS;
075
076    /**
077     * The default DisplaySettings used to select the help lines in the main
078     * body of help
079     */
080    public static final Set DEFAULT_DISPLAY_USAGE_SETTINGS;
081
082    static {
083        final Set fullUsage = new HashSet(DisplaySetting.ALL);
084        fullUsage.remove(DisplaySetting.DISPLAY_ALIASES);
085        fullUsage.remove(DisplaySetting.DISPLAY_GROUP_NAME);
086        fullUsage.remove(DisplaySetting.DISPLAY_OPTIONAL_CHILD_GROUP);
087        DEFAULT_FULL_USAGE_SETTINGS = Collections.unmodifiableSet(fullUsage);
088
089        final Set lineUsage = new HashSet();
090        lineUsage.add(DisplaySetting.DISPLAY_ALIASES);
091        lineUsage.add(DisplaySetting.DISPLAY_GROUP_NAME);
092        lineUsage.add(DisplaySetting.DISPLAY_PARENT_ARGUMENT);
093        DEFAULT_LINE_USAGE_SETTINGS = Collections.unmodifiableSet(lineUsage);
094
095        final Set displayUsage = new HashSet(DisplaySetting.ALL);
096        displayUsage.remove(DisplaySetting.DISPLAY_PARENT_ARGUMENT);
097        DEFAULT_DISPLAY_USAGE_SETTINGS = Collections.unmodifiableSet(displayUsage);
098    }
099
100    private Set fullUsageSettings = new HashSet(DEFAULT_FULL_USAGE_SETTINGS);
101    private Set lineUsageSettings = new HashSet(DEFAULT_LINE_USAGE_SETTINGS);
102    private Set displaySettings = new HashSet(DEFAULT_DISPLAY_USAGE_SETTINGS);
103    private OptionException exception = null;
104    private Group group;
105    private Comparator comparator = null;
106    private String divider = null;
107    private String header = null;
108    private String footer = null;
109    private String shellCommand = "";
110    private PrintWriter out = new PrintWriter(System.out);
111
112    //or should this default to .err?
113    private final String gutterLeft;
114    private final String gutterCenter;
115    private final String gutterRight;
116    private final int pageWidth;
117
118    /**
119     * Creates a new HelpFormatter using the defaults
120     */
121    public HelpFormatter() {
122        this(DEFAULT_GUTTER_LEFT, DEFAULT_GUTTER_CENTER, DEFAULT_GUTTER_RIGHT, DEFAULT_FULL_WIDTH);
123    }
124
125    /**
126     * Creates a new HelpFormatter using the specified parameters
127     * @param gutterLeft the string marking left of screen
128     * @param gutterCenter the string marking center of screen
129     * @param gutterRight the string marking right of screen
130     * @param fullWidth the width of the screen
131     */
132    public HelpFormatter(final String gutterLeft,
133                         final String gutterCenter,
134                         final String gutterRight,
135                         final int fullWidth) {
136        // default the left gutter to empty string
137        this.gutterLeft = (gutterLeft == null) ? DEFAULT_GUTTER_LEFT : gutterLeft;
138
139        // default the center gutter to a single space
140        this.gutterCenter = (gutterCenter == null) ? DEFAULT_GUTTER_CENTER : gutterCenter;
141
142        // default the right gutter to empty string
143        this.gutterRight = (gutterRight == null) ? DEFAULT_GUTTER_RIGHT : gutterRight;
144
145        // calculate the available page width
146        this.pageWidth = fullWidth - this.gutterLeft.length() - this.gutterRight.length();
147
148        // check available page width is valid
149        int availableWidth = fullWidth - pageWidth + this.gutterCenter.length();
150
151        if (availableWidth < 2) {
152            throw new IllegalArgumentException(ResourceHelper.getResourceHelper().getMessage(ResourceConstants.HELPFORMATTER_GUTTER_TOO_LONG));
153        }
154    }
155
156    /**
157     * Prints the Option help.
158     */
159    public void print() {
160        printHeader();
161        printException();
162        printUsage();
163        printHelp();
164        printFooter();
165        out.flush();
166    }
167
168    /**
169     * Prints any error message.
170     */
171    public void printException() {
172        if (exception != null) {
173            printDivider();
174            printWrapped(exception.getMessage());
175        }
176    }
177
178    /**
179     * Prints detailed help per option.
180     */
181    public void printHelp() {
182        printDivider();
183
184        final Option option;
185
186        if ((exception != null) && (exception.getOption() != null)) {
187            option = exception.getOption();
188        } else {
189            option = group;
190        }
191
192        // grab the HelpLines to display
193        final List helpLines = option.helpLines(0, displaySettings, comparator);
194
195        // calculate the maximum width of the usage strings
196        int usageWidth = 0;
197
198        for (final Iterator i = helpLines.iterator(); i.hasNext();) {
199            final HelpLine helpLine = (HelpLine) i.next();
200            final String usage = helpLine.usage(lineUsageSettings, comparator);
201            usageWidth = Math.max(usageWidth, usage.length());
202        }
203
204        // build a blank string to pad wrapped descriptions
205        final StringBuffer blankBuffer = new StringBuffer();
206
207        for (int i = 0; i < usageWidth; i++) {
208            blankBuffer.append(' ');
209        }
210
211        // determine the width available for descriptions
212        final int descriptionWidth = Math.max(1, pageWidth - gutterCenter.length() - usageWidth);
213
214        // display each HelpLine
215        for (final Iterator i = helpLines.iterator(); i.hasNext();) {
216            // grab the HelpLine
217            final HelpLine helpLine = (HelpLine) i.next();
218
219            // wrap the description
220            final List descList = wrap(helpLine.getDescription(), descriptionWidth);
221            final Iterator descriptionIterator = descList.iterator();
222
223            // display usage + first line of description
224            printGutterLeft();
225            pad(helpLine.usage(lineUsageSettings, comparator), usageWidth, out);
226            out.print(gutterCenter);
227            pad((String) descriptionIterator.next(), descriptionWidth, out);
228            printGutterRight();
229            out.println();
230
231            // display padding + remaining lines of description
232            while (descriptionIterator.hasNext()) {
233                printGutterLeft();
234
235                //pad(helpLine.getUsage(),usageWidth,out);
236                out.print(blankBuffer);
237                out.print(gutterCenter);
238                pad((String) descriptionIterator.next(), descriptionWidth, out);
239                printGutterRight();
240                out.println();
241            }
242        }
243
244        printDivider();
245    }
246
247    /**
248     * Prints a single line of usage information (wrapping if necessary)
249     */
250    public void printUsage() {
251        printDivider();
252
253        final StringBuffer buffer = new StringBuffer("Usage:\n");
254        buffer.append(shellCommand).append(' ');
255        group.appendUsage(buffer, fullUsageSettings, comparator, " ");
256        printWrapped(buffer.toString());
257    }
258
259    /**
260     * Prints a header string if necessary
261     */
262    public void printHeader() {
263        if (header != null) {
264            printDivider();
265            printWrapped(header);
266        }
267    }
268
269    /**
270     * Prints a footer string if necessary
271     */
272    public void printFooter() {
273        if (footer != null) {
274            printWrapped(footer);
275            printDivider();
276        }
277    }
278
279    /**
280     * Prints a string wrapped if necessary
281     * @param text the string to wrap
282     */
283    public void printWrapped(final String text) {
284        for (final Iterator i = wrap(text, pageWidth).iterator(); i.hasNext();) {
285            printGutterLeft();
286            pad((String) i.next(), pageWidth, out);
287            printGutterRight();
288            out.println();
289        }
290
291        out.flush();
292    }
293
294    /**
295     * Prints the left gutter string
296     */
297    public void printGutterLeft() {
298        if (gutterLeft != null) {
299            out.print(gutterLeft);
300        }
301    }
302
303    /**
304     * Prints the right gutter string
305     */
306    public void printGutterRight() {
307        if (gutterRight != null) {
308            out.print(gutterRight);
309        }
310    }
311
312    /**
313     * Prints the divider text
314     */
315    public void printDivider() {
316        if (divider != null) {
317            out.println(divider);
318        }
319    }
320
321    protected static void pad(final String text,
322                              final int width,
323                              final PrintWriter writer) {
324        final int left;
325
326        // write the text and record how many characters written
327        if (text == null) {
328            left = 0;
329        } else {
330            writer.write(text);
331            left = text.length();
332        }
333
334        // pad remainder with spaces
335        for (int i = left; i < width; ++i) {
336            writer.write(' ');
337        }
338    }
339
340    protected static List wrap(final String text,
341                               final int width) {
342        // check for valid width
343        if (width < 1) {
344            throw new IllegalArgumentException(ResourceHelper.getResourceHelper().getMessage(ResourceConstants.HELPFORMATTER_WIDTH_TOO_NARROW,
345                                                                                             new Object[] {
346                                                                                                 new Integer(width)
347                                                                                             }));
348        }
349
350        // handle degenerate case
351        if (text == null) {
352            return Collections.singletonList("");
353        }
354
355        final List lines = new ArrayList();
356        final char[] chars = text.toCharArray();
357        int left = 0;
358
359        // for each character in the string
360        while (left < chars.length) {
361            // sync left and right indeces
362            int right = left;
363
364            // move right until we run out of characters, width or find a newline
365            while ((right < chars.length) && (chars[right] != '\n') &&
366                       (right < (left + width + 1))) {
367                right++;
368            }
369
370            // if a newline was found
371            if ((right < chars.length) && (chars[right] == '\n')) {
372                // record the substring
373                final String line = new String(chars, left, right - left);
374                lines.add(line);
375
376                // move to the end of the substring
377                left = right + 1;
378
379                if (left == chars.length) {
380                    lines.add("");
381                }
382
383                // restart the loop
384                continue;
385            }
386
387            // move to the next ideal wrap point
388            right = (left + width) - 1;
389
390            // if we have run out of characters
391            if (chars.length <= right) {
392                // record the substring
393                final String line = new String(chars, left, chars.length - left);
394                lines.add(line);
395
396                // abort the loop
397                break;
398            }
399
400            // back track the substring end until a space is found
401            while ((right >= left) && (chars[right] != ' ')) {
402                right--;
403            }
404
405            // if a space was found
406            if (right >= left) {
407                // record the substring to space
408                final String line = new String(chars, left, right - left);
409                lines.add(line);
410
411                // absorb all the spaces before next substring
412                while ((right < chars.length) && (chars[right] == ' ')) {
413                    right++;
414                }
415
416                left = right;
417
418                // restart the loop
419                continue;
420            }
421
422            // move to the wrap position irrespective of spaces
423            right = Math.min(left + width, chars.length);
424
425            // record the substring
426            final String line = new String(chars, left, right - left);
427            lines.add(line);
428
429            // absorb any the spaces before next substring
430            while ((right < chars.length) && (chars[right] == ' ')) {
431                right++;
432            }
433
434            left = right;
435        }
436
437        return lines;
438    }
439
440    /**
441     * The Comparator to use when sorting Options
442     * @param comparator Comparator to use when sorting Options
443     */
444    public void setComparator(Comparator comparator) {
445        this.comparator = comparator;
446    }
447
448    /**
449     * The DisplaySettings used to select the help lines in the main body of
450     * help
451     *
452     * @param displaySettings the settings to use
453     * @see DisplaySetting
454     */
455    public void setDisplaySettings(Set displaySettings) {
456        this.displaySettings = displaySettings;
457    }
458
459    /**
460     * Sets the string to use as a divider between sections of help
461     * @param divider the dividing string
462     */
463    public void setDivider(String divider) {
464        this.divider = divider;
465    }
466
467    /**
468     * Sets the exception to document
469     * @param exception the exception that occured
470     */
471    public void setException(OptionException exception) {
472        this.exception = exception;
473    }
474
475    /**
476     * Sets the footer text of the help screen
477     * @param footer the footer text
478     */
479    public void setFooter(String footer) {
480        this.footer = footer;
481    }
482
483    /**
484     * The DisplaySettings used to select the elements to display in the
485     * displayed line of full usage information.
486     * @see DisplaySetting
487     * @param fullUsageSettings
488     */
489    public void setFullUsageSettings(Set fullUsageSettings) {
490        this.fullUsageSettings = fullUsageSettings;
491    }
492
493    /**
494     * Sets the Group of Options to document
495     * @param group the options to document
496     */
497    public void setGroup(Group group) {
498        this.group = group;
499    }
500
501    /**
502     * Sets the footer text of the help screen
503     * @param header the footer text
504     */
505    public void setHeader(String header) {
506        this.header = header;
507    }
508
509    /**
510     * Sets the DisplaySettings used to select elements in the per helpline
511     * usage strings.
512     * @see DisplaySetting
513     * @param lineUsageSettings the DisplaySettings to use
514     */
515    public void setLineUsageSettings(Set lineUsageSettings) {
516        this.lineUsageSettings = lineUsageSettings;
517    }
518
519    /**
520     * Sets the command string used to invoke the application
521     * @param shellCommand the invokation command
522     */
523    public void setShellCommand(String shellCommand) {
524        this.shellCommand = shellCommand;
525    }
526
527    /**
528     * @return the Comparator used to sort the Group
529     */
530    public Comparator getComparator() {
531        return comparator;
532    }
533
534    /**
535     * @return the DisplaySettings used to select HelpLines
536     */
537    public Set getDisplaySettings() {
538        return displaySettings;
539    }
540
541    /**
542     * @return the String used as a horizontal section divider
543     */
544    public String getDivider() {
545        return divider;
546    }
547
548    /**
549     * @return the Exception being documented by this HelpFormatter
550     */
551    public OptionException getException() {
552        return exception;
553    }
554
555    /**
556     * @return the help screen footer text
557     */
558    public String getFooter() {
559        return footer;
560    }
561
562    /**
563     * @return the DisplaySettings used in the full usage string
564     */
565    public Set getFullUsageSettings() {
566        return fullUsageSettings;
567    }
568
569    /**
570     * @return the group documented by this HelpFormatter
571     */
572    public Group getGroup() {
573        return group;
574    }
575
576    /**
577     * @return the String used as the central gutter
578     */
579    public String getGutterCenter() {
580        return gutterCenter;
581    }
582
583    /**
584     * @return the String used as the left gutter
585     */
586    public String getGutterLeft() {
587        return gutterLeft;
588    }
589
590    /**
591     * @return the String used as the right gutter
592     */
593    public String getGutterRight() {
594        return gutterRight;
595    }
596
597    /**
598     * @return the help screen header text
599     */
600    public String getHeader() {
601        return header;
602    }
603
604    /**
605     * @return the DisplaySettings used in the per help line usage strings
606     */
607    public Set getLineUsageSettings() {
608        return lineUsageSettings;
609    }
610
611    /**
612     * @return the width of the screen in characters
613     */
614    public int getPageWidth() {
615        return pageWidth;
616    }
617
618    /**
619     * @return the command used to execute the application
620     */
621    public String getShellCommand() {
622        return shellCommand;
623    }
624
625    /**
626     * @param out the PrintWriter to write to
627     */
628    public void setPrintWriter(PrintWriter out) {
629        this.out = out;
630    }
631
632    /**
633     * @return the PrintWriter that will be written to
634     */
635    public PrintWriter getPrintWriter() {
636        return out;
637    }
638}