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     */
017    package org.apache.commons.cli2.util;
018    
019    import java.io.PrintWriter;
020    import java.util.ArrayList;
021    import java.util.Collections;
022    import java.util.Comparator;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Set;
027    
028    import org.apache.commons.cli2.DisplaySetting;
029    import org.apache.commons.cli2.Group;
030    import org.apache.commons.cli2.HelpLine;
031    import org.apache.commons.cli2.Option;
032    import org.apache.commons.cli2.OptionException;
033    import org.apache.commons.cli2.resource.ResourceConstants;
034    import org.apache.commons.cli2.resource.ResourceHelper;
035    
036    /**
037     * Presents on screen help based on the application's Options
038     */
039    public 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    }