001 /*
002 * Copyright 2003-2005 The Apache Software Foundation
003 * Copyright 2005 Stephen McConnell
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * 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 net.dpml.cli.util;
018
019 import java.io.IOException;
020 import java.io.PrintWriter;
021 import java.io.Writer;
022
023 import java.util.ArrayList;
024 import java.util.Collections;
025 import java.util.Comparator;
026 import java.util.HashSet;
027 import java.util.Iterator;
028 import java.util.List;
029 import java.util.Set;
030
031 import net.dpml.cli.DisplaySetting;
032 import net.dpml.cli.Group;
033 import net.dpml.cli.HelpLine;
034 import net.dpml.cli.Option;
035 import net.dpml.cli.OptionException;
036 import net.dpml.cli.resource.ResourceConstants;
037 import net.dpml.cli.resource.ResourceHelper;
038
039 /**
040 * Presents on screen help based on the application's Options
041 *
042 * @author <a href="@PUBLISHER-URL@">@PUBLISHER-NAME@</a>
043 * @version @PROJECT-VERSION@
044 */
045 public class HelpFormatter
046 {
047 /**
048 * The default screen width
049 */
050 public static final int DEFAULT_FULL_WIDTH = 80;
051
052 /**
053 * The default minimum description width.
054 */
055 public static final int DEFAULT_DESCRIPTION_WIDTH = -1;
056
057 /**
058 * The default screen furniture left of screen
059 */
060 public static final String DEFAULT_GUTTER_LEFT = "";
061
062 /**
063 * The default screen furniture right of screen
064 */
065 public static final String DEFAULT_GUTTER_CENTER = " ";
066
067 /**
068 * The default screen furniture between columns
069 */
070 public static final String DEFAULT_GUTTER_RIGHT = "";
071
072 /**
073 * The default DisplaySettings used to select the elements to display in the
074 * displayed line of full usage information.
075 *
076 * @see DisplaySetting
077 */
078 public static final Set DEFAULT_FULL_USAGE_SETTINGS;
079
080 /**
081 * The default DisplaySettings used to select the elements of usage per help
082 * line in the main body of help
083 *
084 * @see DisplaySetting
085 */
086 public static final Set DEFAULT_LINE_USAGE_SETTINGS;
087
088 /**
089 * The default DisplaySettings used to select the help lines in the main
090 * body of help
091 */
092 public static final Set DEFAULT_DISPLAY_USAGE_SETTINGS;
093
094 static
095 {
096 final Set fullUsage = new HashSet( DisplaySetting.ALL );
097 fullUsage.remove( DisplaySetting.DISPLAY_ALIASES );
098 fullUsage.remove( DisplaySetting.DISPLAY_GROUP_NAME );
099 DEFAULT_FULL_USAGE_SETTINGS = Collections.unmodifiableSet( fullUsage );
100
101 final Set lineUsage = new HashSet();
102 lineUsage.add( DisplaySetting.DISPLAY_ALIASES );
103 lineUsage.add( DisplaySetting.DISPLAY_GROUP_NAME );
104 lineUsage.add( DisplaySetting.DISPLAY_PARENT_ARGUMENT );
105 DEFAULT_LINE_USAGE_SETTINGS = Collections.unmodifiableSet( lineUsage );
106
107 final Set displayUsage = new HashSet( DisplaySetting.ALL );
108 displayUsage.remove( DisplaySetting.DISPLAY_PARENT_ARGUMENT );
109 DEFAULT_DISPLAY_USAGE_SETTINGS = Collections.unmodifiableSet( displayUsage );
110 }
111
112 private Set m_fullUsageSettings = new HashSet( DEFAULT_FULL_USAGE_SETTINGS );
113 private Set m_lineUsageSettings = new HashSet( DEFAULT_LINE_USAGE_SETTINGS );
114 private Set m_displaySettings = new HashSet( DEFAULT_DISPLAY_USAGE_SETTINGS );
115 private OptionException m_exception = null;
116 private Group m_group;
117 private Comparator m_comparator = null;
118 private String m_divider = null;
119 private String m_header = null;
120 private String m_footer = null;
121 private String m_shellCommand = "";
122 private PrintWriter m_out = new PrintWriter( System.out );
123
124 //or should this default to .err?
125 private final String m_gutterLeft;
126 private final String m_gutterCenter;
127 private final String m_gutterRight;
128 private final int m_pageWidth;
129 private final int m_descriptionWidth;
130
131 /**
132 * Creates a new HelpFormatter using the defaults
133 */
134 public HelpFormatter()
135 {
136 this(
137 DEFAULT_GUTTER_LEFT, DEFAULT_GUTTER_CENTER, DEFAULT_GUTTER_RIGHT,
138 DEFAULT_FULL_WIDTH, DEFAULT_DESCRIPTION_WIDTH );
139 }
140
141 /**
142 * Creates a new HelpFormatter using the specified parameters
143 * @param gutterLeft the string marking left of screen
144 * @param gutterCenter the string marking center of screen
145 * @param gutterRight the string marking right of screen
146 * @param fullWidth the width of the screen
147 */
148 public HelpFormatter(
149 final String gutterLeft, final String gutterCenter, final String gutterRight,
150 final int fullWidth )
151 {
152 this( gutterLeft, gutterCenter, gutterRight, fullWidth, DEFAULT_DESCRIPTION_WIDTH );
153 }
154
155 /**
156 * Creates a new HelpFormatter using the specified parameters
157 * @param gutterLeft the string marking left of screen
158 * @param gutterCenter the string marking center of screen
159 * @param gutterRight the string marking right of screen
160 * @param fullWidth the width of the screen
161 * @param descriptionWidth the minimum description width
162 */
163 public HelpFormatter(
164 final String gutterLeft, final String gutterCenter, final String gutterRight,
165 final int fullWidth, final int descriptionWidth )
166 {
167 // default the left gutter to empty string
168 if( null == gutterLeft )
169 {
170 m_gutterLeft = DEFAULT_GUTTER_LEFT;
171 }
172 else
173 {
174 m_gutterLeft = gutterLeft;
175 }
176
177 if( null == gutterCenter )
178 {
179 m_gutterCenter = DEFAULT_GUTTER_CENTER;
180 }
181 else
182 {
183 m_gutterCenter = gutterCenter;
184 }
185
186 if( null == gutterRight )
187 {
188 m_gutterRight = DEFAULT_GUTTER_RIGHT;
189 }
190 else
191 {
192 m_gutterRight = gutterRight;
193 }
194
195 m_descriptionWidth = descriptionWidth;
196
197 // calculate the available page width
198 m_pageWidth = fullWidth - m_gutterLeft.length() - m_gutterRight.length();
199
200 // check available page width is valid
201 int availableWidth = fullWidth - m_pageWidth + m_gutterCenter.length();
202
203 if( availableWidth < 2 )
204 {
205 throw new IllegalArgumentException(
206 ResourceHelper.getResourceHelper().getMessage(
207 ResourceConstants.HELPFORMATTER_GUTTER_TOO_LONG ) );
208 }
209 }
210
211 /**
212 * Prints the Option help.
213 * @throws IOException if an error occurs
214 */
215 public void print() throws IOException
216 {
217 printHeader();
218 printException();
219 printUsage();
220 printHelp();
221 printFooter();
222 m_out.flush();
223 }
224
225 /**
226 * Prints any error message.
227 * @throws IOException if an error occurs
228 */
229 public void printException() throws IOException
230 {
231 if( m_exception != null )
232 {
233 printDivider();
234 printWrapped( m_exception.getMessage() );
235 }
236 }
237
238 /**
239 * Prints detailed help per option.
240 * @throws IOException if an error occurs
241 */
242 public void printHelp() throws IOException
243 {
244 printDivider();
245 final Option option;
246 if( ( m_exception != null ) && ( m_exception.getOption() != null ) )
247 {
248 option = m_exception.getOption();
249 }
250 else
251 {
252 option = m_group;
253 }
254
255 // grab the HelpLines to display
256 final List helpLines = option.helpLines( 0, m_displaySettings, m_comparator );
257
258 // calculate the maximum width of the usage strings
259 int usageWidth = 0;
260
261 for( final Iterator i = helpLines.iterator(); i.hasNext();)
262 {
263 final HelpLine helpLine = (HelpLine) i.next();
264 final String usage = helpLine.usage( m_lineUsageSettings, m_comparator );
265 usageWidth = Math.max( usageWidth, usage.length() );
266 }
267
268 //
269 // add check for an overriding description max width (needed in complex
270 // usage scenarios)
271 //
272
273 if( m_descriptionWidth > -1 )
274 {
275 int max = m_pageWidth - m_descriptionWidth;
276 if( usageWidth > max )
277 {
278 usageWidth = max;
279 }
280 }
281
282 // build a blank string to pad wrapped descriptions
283 final StringBuffer blankBuffer = new StringBuffer();
284
285 for( int i = 0; i < usageWidth; i++ )
286 {
287 blankBuffer.append( ' ' );
288 }
289
290 // determine the width available for descriptions
291 final int descriptionWidth =
292 Math.max( 1, m_pageWidth - m_gutterCenter.length() - usageWidth );
293
294 // display each HelpLine
295 for( final Iterator i = helpLines.iterator(); i.hasNext();)
296 {
297 // grab the HelpLine
298 final HelpLine helpLine = (HelpLine) i.next();
299
300 // wrap the description
301 final List descList = wrap( helpLine.getDescription(), descriptionWidth );
302 final Iterator descriptionIterator = descList.iterator();
303
304 // display usage + first line of description
305 printGutterLeft();
306 pad( helpLine.usage( m_lineUsageSettings, m_comparator ), usageWidth, m_out );
307 m_out.print( m_gutterCenter );
308 pad( (String) descriptionIterator.next(), descriptionWidth, m_out );
309 printGutterRight();
310 m_out.println();
311
312 // display padding + remaining lines of description
313 while( descriptionIterator.hasNext() )
314 {
315 printGutterLeft();
316
317 //pad(helpLine.getUsage(),usageWidth,m_out);
318 m_out.print( blankBuffer );
319 m_out.print( m_gutterCenter );
320 pad( (String) descriptionIterator.next(), descriptionWidth, m_out );
321 printGutterRight();
322 m_out.println();
323 }
324 }
325 printDivider();
326 }
327
328 /**
329 * Prints a single line of usage information (wrapping if necessary)
330 * @throws IOException if an error occurs
331 */
332 public void printUsage() throws IOException
333 {
334 printDivider();
335 final StringBuffer buffer = new StringBuffer( "Usage:\n" );
336 buffer.append( m_shellCommand ).append( ' ' );
337 String separator = getSeparator();
338 m_group.appendUsage( buffer, m_fullUsageSettings, m_comparator, separator );
339 printWrapped( buffer.toString() );
340 }
341
342 private String getSeparator()
343 {
344 if( m_group.getMaximum() == 1 )
345 {
346 return " | ";
347 }
348 else
349 {
350 return " ";
351 }
352 }
353
354 /**
355 * Prints a m_header string if necessary
356 * @throws IOException if an error occurs
357 */
358 public void printHeader() throws IOException
359 {
360 if( m_header != null )
361 {
362 printDivider();
363 printWrapped( m_header );
364 }
365 }
366
367 /**
368 * Prints a m_footer string if necessary
369 * @throws IOException if an error occurs
370 */
371 public void printFooter() throws IOException
372 {
373 if( m_footer != null )
374 {
375 printWrapped( m_footer );
376 printDivider();
377 }
378 }
379
380 /**
381 * Prints a string wrapped if necessary
382 * @param text the string to wrap
383 * @throws IOException if an error occurs
384 */
385 protected void printWrapped( final String text ) throws IOException
386 {
387 for( final Iterator i = wrap( text, m_pageWidth ).iterator(); i.hasNext();)
388 {
389 printGutterLeft();
390 pad( (String) i.next(), m_pageWidth, m_out );
391 printGutterRight();
392 m_out.println();
393 }
394 }
395
396 /**
397 * Prints the left gutter string
398 */
399 public void printGutterLeft()
400 {
401 if( m_gutterLeft != null )
402 {
403 m_out.print( m_gutterLeft );
404 }
405 }
406
407 /**
408 * Prints the right gutter string
409 */
410 public void printGutterRight()
411 {
412 if( m_gutterRight != null )
413 {
414 m_out.print( m_gutterRight );
415 }
416 }
417
418 /**
419 * Prints the m_divider text
420 */
421 public void printDivider()
422 {
423 if( m_divider != null )
424 {
425 m_out.println( m_divider );
426 }
427 }
428
429 /**
430 * Pad the supplied string.
431 * @param text the text to pad
432 * @param width the padding width
433 * @param writer the writer
434 * @exception IOException if an I/O error occurs
435 */
436 protected static void pad(
437 final String text, final int width, final Writer writer )
438 throws IOException
439 {
440 final int left;
441
442 // write the text and record how many characters written
443 if ( text == null )
444 {
445 left = 0;
446 }
447 else
448 {
449 writer.write( text );
450 left = text.length();
451 }
452
453 // pad remainder with spaces
454 for( int i = left; i < width; ++i )
455 {
456 writer.write( ' ' );
457 }
458 }
459
460 /**
461 * Return a list of strings resulting from the wrapping of a supplied
462 * target string.
463 * @param text the target string to wrap
464 * @param width the wrappping width
465 * @return the list of wrapped fragments
466 */
467 protected static List wrap( final String text, final int width )
468 {
469 // check for valid width
470 if( width < 1 )
471 {
472 throw new IllegalArgumentException(
473 ResourceHelper.getResourceHelper().getMessage(
474 ResourceConstants.HELPFORMATTER_WIDTH_TOO_NARROW,
475 new Object[]{new Integer( width )} ) );
476 }
477
478 // handle degenerate case
479 if( text == null )
480 {
481 return Collections.singletonList( "" );
482 }
483
484 final List lines = new ArrayList();
485 final char[] chars = text.toCharArray();
486 int left = 0;
487
488 // for each character in the string
489 while( left < chars.length )
490 {
491 // sync left and right indeces
492 int right = left;
493
494 // move right until we run m_out of characters, width or find a newline
495 while(
496 ( right < chars.length )
497 && ( chars[right] != '\n' )
498 && ( right < ( left + width + 1 ) ) )
499 {
500 right++;
501 }
502
503 // if a newline was found
504 if( ( right < chars.length ) && ( chars[right] == '\n' ) )
505 {
506 // record the substring
507 final String line = new String( chars, left, right - left );
508 lines.add( line );
509
510 // move to the end of the substring
511 left = right + 1;
512
513 if( left == chars.length )
514 {
515 lines.add( "" );
516 }
517
518 // restart the loop
519 continue;
520 }
521
522 // move to the next ideal wrap point
523 right = ( left + width ) - 1;
524
525 // if we have run m_out of characters
526 if( chars.length <= right )
527 {
528 // record the substring
529 final String line = new String( chars, left, chars.length - left );
530 lines.add( line );
531
532 // abort the loop
533 break;
534 }
535
536 // back track the substring end until a space is found
537 while( ( right >= left ) && ( chars[right] != ' ' ) )
538 {
539 right--;
540 }
541
542 // if a space was found
543 if( right >= left )
544 {
545 // record the substring to space
546 final String line = new String( chars, left, right - left );
547 lines.add( line );
548
549 // absorb all the spaces before next substring
550 while( ( right < chars.length ) && ( chars[right] == ' ' ) )
551 {
552 right++;
553 }
554
555 left = right;
556
557 // restart the loop
558 continue;
559 }
560
561 // move to the wrap position irrespective of spaces
562 right = Math.min( left + width, chars.length );
563
564 // record the substring
565 final String line = new String( chars, left, right - left );
566 lines.add( line );
567
568 // absorb any the spaces before next substring
569 while( ( right < chars.length ) && ( chars[right] == ' ' ) )
570 {
571 right++;
572 }
573
574 left = right;
575 }
576
577 return lines;
578 }
579
580 /**
581 * The Comparator to use when sorting Options
582 * @param comparator Comparator to use when sorting Options
583 */
584 public void setComparator( Comparator comparator )
585 {
586 m_comparator = comparator;
587 }
588
589 /**
590 * The DisplaySettings used to select the help lines in the main body of
591 * help
592 *
593 * @param displaySettings the settings to use
594 * @see DisplaySetting
595 */
596 public void setDisplaySettings( Set displaySettings )
597 {
598 m_displaySettings = displaySettings;
599 }
600
601 /**
602 * Sets the string to use as a m_divider between sections of help
603 * @param divider the dividing string
604 */
605 public void setDivider( String divider )
606 {
607 m_divider = divider;
608 }
609
610 /**
611 * Sets the exception to document
612 * @param exception the exception that occured
613 */
614 public void setException( OptionException exception )
615 {
616 m_exception = exception;
617 }
618
619 /**
620 * Sets the footer text of the help screen
621 * @param footer the footer text
622 */
623 public void setFooter( String footer )
624 {
625 m_footer = footer;
626 }
627
628 /**
629 * The DisplaySettings used to select the elements to display in the
630 * displayed line of full usage information.
631 * @see DisplaySetting
632 * @param fullUsageSettings the full usage settings
633 */
634 public void setFullUsageSettings( Set fullUsageSettings )
635 {
636 m_fullUsageSettings = fullUsageSettings;
637 }
638
639 /**
640 * Sets the Group of Options to document
641 * @param group the options to document
642 */
643 public void setGroup( Group group )
644 {
645 m_group = group;
646 }
647
648 /**
649 * Sets the header text of the help screen
650 * @param header the m_footer text
651 */
652 public void setHeader( String header )
653 {
654 m_header = header;
655 }
656
657 /**
658 * Sets the DisplaySettings used to select elements in the per helpline
659 * usage strings.
660 * @see DisplaySetting
661 * @param lineUsageSettings the DisplaySettings to use
662 */
663 public void setLineUsageSettings( Set lineUsageSettings )
664 {
665 m_lineUsageSettings = lineUsageSettings;
666 }
667
668 /**
669 * Sets the command string used to invoke the application
670 * @param shellCommand the invocation command
671 */
672 public void setShellCommand( String shellCommand )
673 {
674 m_shellCommand = shellCommand;
675 }
676
677 /**
678 * Return the comparator.
679 * @return the Comparator used to sort the Group
680 */
681 public Comparator getComparator()
682 {
683 return m_comparator;
684 }
685
686 /**
687 * Return the display settings.
688 * @return the DisplaySettings used to select HelpLines
689 */
690 public Set getDisplaySettings()
691 {
692 return m_displaySettings;
693 }
694
695 /**
696 * Return the divider.
697 * @return the String used as a horizontal section m_divider
698 */
699 public String getDivider()
700 {
701 return m_divider;
702 }
703
704 /**
705 * Return the option exception
706 * @return the Exception being documented by this HelpFormatter
707 */
708 public OptionException getException()
709 {
710 return m_exception;
711 }
712
713 /**
714 * Return the footer text.
715 * @return the help screen footer text
716 */
717 public String getFooter()
718 {
719 return m_footer;
720 }
721
722 /**
723 * Return the full usage display settings.
724 * @return the DisplaySettings used in the full usage string
725 */
726 public Set getFullUsageSettings()
727 {
728 return m_fullUsageSettings;
729 }
730
731 /**
732 * Return the group.
733 * @return the group documented by this HelpFormatter
734 */
735 public Group getGroup()
736 {
737 return m_group;
738 }
739
740 /**
741 * Return the gutter center string.
742 * @return the String used as the central gutter
743 */
744 public String getGutterCenter()
745 {
746 return m_gutterCenter;
747 }
748
749 /**
750 * Return the gutter left string.
751 * @return the String used as the left gutter
752 */
753 public String getGutterLeft()
754 {
755 return m_gutterLeft;
756 }
757
758 /**
759 * Return the gutter right string.
760 * @return the String used as the right gutter
761 */
762 public String getGutterRight()
763 {
764 return m_gutterRight;
765 }
766
767 /**
768 * Return the header string.
769 * @return the help screen header text
770 */
771 public String getHeader()
772 {
773 return m_header;
774 }
775
776 /**
777 * Return the line usage settings.
778 * @return the DisplaySettings used in the per help line usage strings
779 */
780 public Set getLineUsageSettings()
781 {
782 return m_lineUsageSettings;
783 }
784
785 /**
786 * Return the page width.
787 * @return the width of the screen in characters
788 */
789 public int getPageWidth()
790 {
791 return m_pageWidth;
792 }
793
794 /**
795 * Return the shell command.
796 * @return the command used to execute the application
797 */
798 public String getShellCommand()
799 {
800 return m_shellCommand;
801 }
802
803 /**
804 * Set the print writer.
805 * @param out the PrintWriter to write to
806 */
807 public void setPrintWriter( PrintWriter out )
808 {
809 m_out = out;
810 }
811
812 /**
813 * Return the print writer.
814 * @return the PrintWriter that will be written to
815 */
816 public PrintWriter getPrintWriter()
817 {
818 return m_out;
819 }
820 }