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="http://www.dpml.net">Digital Product Meta Library</a>
043     * @version 1.0.0
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    }