View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.cli2.util;
18  
19  import java.io.PrintWriter;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.HashSet;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Set;
27  
28  import org.apache.commons.cli2.DisplaySetting;
29  import org.apache.commons.cli2.Group;
30  import org.apache.commons.cli2.HelpLine;
31  import org.apache.commons.cli2.Option;
32  import org.apache.commons.cli2.OptionException;
33  import org.apache.commons.cli2.resource.ResourceConstants;
34  import org.apache.commons.cli2.resource.ResourceHelper;
35  
36  /**
37   * Presents on screen help based on the application's Options
38   */
39  public class HelpFormatter {
40      /**
41       * The default screen width
42       */
43      public static final int DEFAULT_FULL_WIDTH = 80;
44  
45      /**
46       * The default screen furniture left of screen
47       */
48      public static final String DEFAULT_GUTTER_LEFT = "";
49  
50      /**
51       * The default screen furniture right of screen
52       */
53      public static final String DEFAULT_GUTTER_CENTER = "    ";
54  
55      /**
56       * The default screen furniture between columns
57       */
58      public static final String DEFAULT_GUTTER_RIGHT = "";
59  
60      /**
61       * The default DisplaySettings used to select the elements to display in the
62       * displayed line of full usage information.
63       *
64       * @see DisplaySetting
65       */
66      public static final Set DEFAULT_FULL_USAGE_SETTINGS;
67  
68      /**
69       * The default DisplaySettings used to select the elements of usage per help
70       * line in the main body of help
71       *
72       * @see DisplaySetting
73       */
74      public static final Set DEFAULT_LINE_USAGE_SETTINGS;
75  
76      /**
77       * The default DisplaySettings used to select the help lines in the main
78       * body of help
79       */
80      public static final Set DEFAULT_DISPLAY_USAGE_SETTINGS;
81  
82      static {
83          final Set fullUsage = new HashSet(DisplaySetting.ALL);
84          fullUsage.remove(DisplaySetting.DISPLAY_ALIASES);
85          fullUsage.remove(DisplaySetting.DISPLAY_GROUP_NAME);
86          fullUsage.remove(DisplaySetting.DISPLAY_OPTIONAL_CHILD_GROUP);
87          DEFAULT_FULL_USAGE_SETTINGS = Collections.unmodifiableSet(fullUsage);
88  
89          final Set lineUsage = new HashSet();
90          lineUsage.add(DisplaySetting.DISPLAY_ALIASES);
91          lineUsage.add(DisplaySetting.DISPLAY_GROUP_NAME);
92          lineUsage.add(DisplaySetting.DISPLAY_PARENT_ARGUMENT);
93          DEFAULT_LINE_USAGE_SETTINGS = Collections.unmodifiableSet(lineUsage);
94  
95          final Set displayUsage = new HashSet(DisplaySetting.ALL);
96          displayUsage.remove(DisplaySetting.DISPLAY_PARENT_ARGUMENT);
97          DEFAULT_DISPLAY_USAGE_SETTINGS = Collections.unmodifiableSet(displayUsage);
98      }
99  
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 }