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 */
017package net.dpml.cli.util;
018
019import java.io.IOException;
020import java.io.PrintWriter;
021import java.io.Writer;
022
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.Comparator;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Set;
030
031import net.dpml.cli.DisplaySetting;
032import net.dpml.cli.Group;
033import net.dpml.cli.HelpLine;
034import net.dpml.cli.Option;
035import net.dpml.cli.OptionException;
036import net.dpml.cli.resource.ResourceConstants;
037import 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 */
045public 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}