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.option;
018    
019    import java.util.ArrayList;
020    import java.util.Collection;
021    import java.util.Collections;
022    import java.util.Comparator;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.ListIterator;
027    import java.util.Map;
028    import java.util.Set;
029    import java.util.SortedMap;
030    import java.util.TreeMap;
031    
032    import net.dpml.cli.Argument;
033    import net.dpml.cli.DisplaySetting;
034    import net.dpml.cli.Group;
035    import net.dpml.cli.HelpLine;
036    import net.dpml.cli.Option;
037    import net.dpml.cli.OptionException;
038    import net.dpml.cli.WriteableCommandLine;
039    import net.dpml.cli.resource.ResourceConstants;
040    
041    /**
042     * An implementation of Group
043     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
044     * @version 1.0.0
045     */
046    public class GroupImpl extends OptionImpl implements Group 
047    {
048        private final String m_name;
049        private final String m_description;
050        private final List m_options;
051        private final int m_minimum;
052        private final int m_maximum;
053        private final List m_anonymous;
054        private final SortedMap m_optionMap;
055        private final Set m_prefixes;
056    
057        /**
058         * Creates a new GroupImpl using the specified parameters.
059         *
060         * @param options the Options and Arguments that make up the Group
061         * @param name the name of this Group, or null
062         * @param description a description of this Group
063         * @param minimum the minimum number of Options for a valid CommandLine
064         * @param maximum the maximum number of Options for a valid CommandLine
065         */
066        public GroupImpl(
067          final List options, final String name, final String description,
068          final int minimum, final int maximum )
069        {
070            super( 0, false );
071    
072            m_name = name;
073            m_description = description;
074            m_minimum = minimum;
075            m_maximum = maximum;
076    
077            // store a copy of the options to be used by the 
078            // help methods
079            m_options = Collections.unmodifiableList( options );
080    
081            // m_anonymous Argument temporary storage
082            final List newAnonymous = new ArrayList();
083    
084            // map (key=trigger & value=Option) temporary storage
085            final SortedMap newOptionMap = new TreeMap( ReverseStringComparator.getInstance() );
086    
087            // prefixes temporary storage
088            final Set newPrefixes = new HashSet();
089    
090            // process the options
091            for( final Iterator i = options.iterator(); i.hasNext();)
092            {
093                final Option option = (Option) i.next();
094                if( option instanceof Argument ) 
095                {
096                    i.remove();
097                    newAnonymous.add( option );
098                } 
099                else
100                {
101                    final Set triggers = option.getTriggers();
102                    for( Iterator j = triggers.iterator(); j.hasNext();)
103                    {
104                        newOptionMap.put( j.next(), option );
105                    }
106                    // store the prefixes
107                    newPrefixes.addAll( option.getPrefixes() );
108                }
109            }
110    
111            m_anonymous = Collections.unmodifiableList( newAnonymous );
112            m_optionMap = Collections.unmodifiableSortedMap( newOptionMap );
113            m_prefixes = Collections.unmodifiableSet( newPrefixes );
114        }
115    
116        /**
117         * Indicates whether this Option will be able to process the particular
118         * argument.
119         * 
120         * @param commandLine the CommandLine object to store defaults in
121         * @param arg the argument to be tested
122         * @return true if the argument can be processed by this Option
123         */
124        public boolean canProcess(
125          final WriteableCommandLine commandLine, final String arg )
126        {
127            if( arg == null )
128            {
129                return false;
130            }
131    
132            // if arg does not require bursting
133            if( m_optionMap.containsKey( arg ) )
134            {
135                return true;
136            }
137    
138            // filter
139            final Map tailMap = m_optionMap.tailMap( arg );
140    
141            // check if bursting is required
142            for( final Iterator iter = tailMap.values().iterator(); iter.hasNext();)
143            {
144                final Option option = (Option) iter.next();
145                if( option.canProcess( commandLine, arg ) )
146                {
147                    return true;
148                }
149            }
150            
151            if( commandLine.looksLikeOption( arg ) )
152            {
153                return false;
154            }
155    
156            // m_anonymous argument(s) means we can process it
157            if( m_anonymous.size() > 0 )
158            {
159                return true;
160            }
161    
162            return false;
163        }
164    
165        /**
166         * Identifies the argument prefixes that should be considered options. This
167         * is used to identify whether a given string looks like an option or an
168         * argument value. Typically an option would return the set [--,-] while
169         * switches might offer [-,+].
170         * 
171         * The returned Set must not be null.
172         * 
173         * @return The set of prefixes for this Option
174         */
175        public Set getPrefixes()
176        {
177            return m_prefixes;
178        }
179    
180        /**
181         * Identifies the argument prefixes that should trigger this option. This
182         * is used to decide which of many Options should be tried when processing
183         * a given argument string.
184         * 
185         * The returned Set must not be null.
186         * 
187         * @return The set of triggers for this Option
188         */
189        public Set getTriggers()
190        {
191            return m_optionMap.keySet();
192        }
193    
194        /**
195         * Processes String arguments into a CommandLine.
196         * 
197         * The iterator will initially point at the first argument to be processed
198         * and at the end of the method should point to the first argument not
199         * processed. This method MUST process at least one argument from the
200         * ListIterator.
201         * 
202         * @param commandLine the CommandLine object to store results in
203         * @param arguments the arguments to process
204         * @throws OptionException if any problems occur
205         */
206        public void process(
207          final WriteableCommandLine commandLine, final ListIterator arguments )
208          throws OptionException
209        {
210            String previous = null;
211    
212            // [START process each command line token
213            while( arguments.hasNext() )
214            {
215                // grab the next argument
216                final String arg = (String) arguments.next();
217    
218                // if we have just tried to process this instance
219                if( arg == previous )
220                {
221                    // rollback and abort
222                    arguments.previous();
223                    break;
224                }
225    
226                // remember last processed instance
227                previous = arg;
228    
229                final Option opt = (Option) m_optionMap.get( arg );
230    
231                // option found
232                if( opt != null )
233                {
234                    arguments.previous();
235                    opt.process( commandLine, arguments );
236                }
237                // [START option NOT found
238                else
239                {
240                    // it might be an m_anonymous argument continue search
241                    // [START argument may be m_anonymous
242                    if( commandLine.looksLikeOption( arg ) )
243                    {
244                        // narrow the search
245                        final Collection values = m_optionMap.tailMap( arg ).values();
246                        boolean foundMemberOption = false;
247                        for( Iterator i = values.iterator(); i.hasNext() && !foundMemberOption;)
248                        {
249                            final Option option = (Option) i.next();
250                            if( option.canProcess( commandLine, arg ) )
251                            {
252                                foundMemberOption = true;
253                                arguments.previous();
254                                option.process( commandLine, arguments );
255                            }
256                        }
257    
258                        // back track and abort this group if necessary
259                        if( !foundMemberOption )
260                        {
261                            arguments.previous();
262                            return;
263                        }
264                        
265                    } // [END argument may be m_anonymous
266                    // [START argument is NOT m_anonymous
267                    else 
268                    {
269                        // move iterator back, current value not used
270                        arguments.previous();
271    
272                        // if there are no m_anonymous arguments then this group can't
273                        // process the argument
274                        if( m_anonymous.isEmpty() )
275                        {
276                            break;
277                        }
278    
279                        // why do we iterate over all m_anonymous arguments?
280                        // canProcess will always return true?
281                        for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
282                        {
283                            final Argument argument = (Argument) i.next();
284                            if( argument.canProcess( commandLine, arguments ) )
285                            {
286                                argument.process( commandLine, arguments );
287                            }
288                        }
289                    } // [END argument is NOT m_anonymous
290                } // [END option NOT found
291            } // [END process each command line token
292        }
293    
294        /**
295         * Checks that the supplied CommandLine is valid with respect to this
296         * option.
297         * 
298         * @param commandLine the CommandLine to check.
299         * @throws OptionException if the CommandLine is not valid.
300         */
301        public void validate( final WriteableCommandLine commandLine ) throws OptionException 
302        {
303            // number of options found
304            int present = 0;
305    
306            // reference to first unexpected option
307            Option unexpected = null;
308    
309            for( final Iterator i = m_options.iterator(); i.hasNext();)
310            {
311                final Option option = (Option) i.next();
312    
313                // if the child option is required then validate it
314                if( option.isRequired() )
315                {
316                    option.validate( commandLine );
317                }
318    
319                if( option instanceof Group )
320                {
321                    option.validate( commandLine );
322                }
323    
324                // if the child option is present then validate it
325                if( commandLine.hasOption( option ) )
326                {
327                    if( ++present > m_maximum )
328                    {
329                        unexpected = option;
330                        break;
331                    }
332                    option.validate( commandLine );
333                }
334            }
335    
336            // too many options
337            if( unexpected != null )
338            {
339                throw new OptionException(
340                  this,
341                  ResourceConstants.UNEXPECTED_TOKEN,
342                  unexpected.getPreferredName() );
343            }
344    
345            // too few option
346            if( present < m_minimum )
347            {
348                throw new OptionException(
349                  this,
350                  ResourceConstants.MISSING_OPTION );
351            }
352    
353            // validate each m_anonymous argument
354            for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
355            {
356                final Option option = (Option) i.next();
357                option.validate( commandLine );
358            }
359        }
360    
361        /**
362         * The preferred name of an option is used for generating help and usage
363         * information.
364         * 
365         * @return The preferred name of the option
366         */
367        public String getPreferredName()
368        {
369            return m_name;
370        }
371    
372        /**
373         * Returns a description of the option. This string is used to build help
374         * messages as in the HelpFormatter.
375         * 
376         * @see net.dpml.cli.util.HelpFormatter
377         * @return a description of the option.
378         */
379        public String getDescription() 
380        {
381            return m_description;
382        }
383    
384        /**
385         * Appends usage information to the specified StringBuffer
386         * 
387         * @param buffer the buffer to append to
388         * @param helpSettings a set of display settings @see DisplaySetting
389         * @param comp a comparator used to sort the Options
390         */
391        public void appendUsage(
392          final StringBuffer buffer, final Set helpSettings, final Comparator comp ) 
393        {
394            if( getMaximum() == 1 )
395            {
396                appendUsage( buffer, helpSettings, comp, "|" );
397            }
398            else
399            {
400                appendUsage( buffer, helpSettings, comp, " " );
401            }
402        }
403    
404        /**
405         * Appends usage information to the specified StringBuffer
406         * 
407         * @param buffer the buffer to append to
408         * @param helpSettings a set of display settings @see DisplaySetting
409         * @param comp a comparator used to sort the Options
410         * @param separator the String used to separate member Options 
411         */
412        public void appendUsage(
413          final StringBuffer buffer, final Set helpSettings, final Comparator comp,
414          final String separator )
415        {
416            final Set helpSettingsCopy = new HashSet( helpSettings );
417    
418            final boolean optional =
419              ( m_minimum == 0 ) 
420              && helpSettingsCopy.contains( DisplaySetting.DISPLAY_OPTIONAL );
421    
422            final boolean expanded =
423              ( m_name == null ) 
424              || helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED );
425    
426            final boolean named =
427              !expanded 
428              || ( ( m_name != null ) && helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_NAME ) );
429    
430            final boolean arguments = 
431              helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_ARGUMENT );
432    
433            final boolean outer = 
434              helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_OUTER );
435    
436            helpSettingsCopy.remove( DisplaySetting.DISPLAY_GROUP_OUTER );
437    
438            final boolean both = named && expanded;
439    
440            if( optional )
441            {
442                buffer.append( '[' );
443            }
444    
445            if( named )
446            {
447                buffer.append( m_name );
448            }
449    
450            if( both )
451            {
452                buffer.append( " (" );
453            }
454    
455            if( expanded )
456            {
457                final Set childSettings;
458    
459                if( !helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ) )
460                {
461                    childSettings = DisplaySetting.NONE;
462                }
463                else
464                {
465                    childSettings = new HashSet( helpSettingsCopy );
466                    childSettings.remove( DisplaySetting.DISPLAY_OPTIONAL );
467                }
468    
469                // grab a list of the group's options.
470                final List list;
471    
472                if( comp == null )
473                {
474                    // default to using the initial order
475                    list = m_options;
476                } 
477                else
478                {
479                    // sort options if comparator is supplied
480                    list = new ArrayList( m_options );
481                    Collections.sort( list, comp );
482                }
483    
484                // for each option.
485                for( final Iterator i = list.iterator(); i.hasNext();)
486                {
487                    final Option option = (Option) i.next();
488    
489                    // append usage information
490                    option.appendUsage( buffer, childSettings, comp );
491    
492                    // add separators as needed
493                    if( i.hasNext() )
494                    {
495                        buffer.append( separator );
496                    }
497                }
498            }
499    
500            if( both ) 
501            {
502                buffer.append( ')' );
503            }
504    
505            if( optional && outer )
506            {
507                buffer.append( ']' );
508            }
509    
510            if( arguments )
511            {
512                for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
513                {
514                    buffer.append( ' ' );
515                    final Option option = (Option) i.next();
516                    option.appendUsage( buffer, helpSettingsCopy, comp );
517                }
518            }
519    
520            if( optional && !outer )
521            {
522                buffer.append( ']' );
523            }
524        }
525    
526        /**
527         * Builds up a list of HelpLineImpl instances to be presented by HelpFormatter.
528         * 
529         * @see HelpLine
530         * @see net.dpml.cli.util.HelpFormatter
531         * @param depth the initial indent depth
532         * @param helpSettings the HelpSettings that should be applied
533         * @param comp a comparator used to sort options when applicable.
534         * @return a List of HelpLineImpl objects
535         */
536        public List helpLines(
537          final int depth, final Set helpSettings, final Comparator comp )
538        {
539            final List helpLines = new ArrayList();
540    
541            if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_NAME ) )
542            {
543                final HelpLine helpLine = new HelpLineImpl( this, depth );
544                helpLines.add( helpLine );
545            }
546    
547            if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ) )
548            {
549                // grab a list of the group's options.
550                final List list;
551    
552                if( comp == null )
553                {
554                    // default to using the initial order
555                    list = m_options;
556                } 
557                else
558                {
559                    // sort options if comparator is supplied
560                    list = new ArrayList( m_options );
561                    Collections.sort( list, comp );
562                }
563    
564                // for each option
565                for( final Iterator i = list.iterator(); i.hasNext();)
566                {
567                    final Option option = (Option) i.next();
568                    helpLines.addAll( option.helpLines( depth + 1, helpSettings, comp ) );
569                }
570            }
571    
572            if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_ARGUMENT ) )
573            {
574                for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
575                {
576                    final Option option = (Option) i.next();
577                    helpLines.addAll( option.helpLines( depth + 1, helpSettings, comp ) );
578                }
579            }
580    
581            return helpLines;
582        }
583    
584        /**
585         * Gets the member Options of thie Group.
586         * Note this does not include any Arguments
587         * @return only the non Argument Options of the Group
588         */
589        public List getOptions()
590        {
591            return m_options;
592        }
593    
594        /**
595         * Gets the m_anonymous Arguments of this Group.
596         * @return the Argument options of this Group
597         */
598        public List getAnonymous() 
599        {
600            return m_anonymous;
601        }
602    
603       /**
604        * Recursively searches for an option with the supplied trigger.
605        *
606        * @param trigger the trigger to search for.
607        * @return the matching option or null.
608        */
609        public Option findOption( final String trigger ) 
610        {
611            final Iterator i = getOptions().iterator();
612    
613            while( i.hasNext() ) 
614            {
615                final Option option = (Option) i.next();
616                final Option found = option.findOption( trigger );
617                if( found != null )
618                {
619                    return found;
620                }
621            }
622            return null;
623        }
624    
625        /**
626         * Retrieves the minimum number of values required for a valid Argument
627         *
628         * @return the minimum number of values
629         */
630        public int getMinimum()
631        {
632            return m_minimum;
633        }
634    
635        /**
636         * Retrieves the maximum number of values acceptable for a valid Argument
637         *
638         * @return the maximum number of values
639         */
640        public int getMaximum() 
641        {
642            return m_maximum;
643        }
644    
645        /**
646         * Indicates whether argument values must be present for the CommandLine to
647         * be valid.
648         *
649         * @see #getMinimum()
650         * @see #getMaximum()
651         * @return true iff the CommandLine will be invalid without at least one 
652         *         value
653         */
654        public boolean isRequired()
655        {
656            return getMinimum() > 0;
657        }
658    
659       /**
660        * Process defaults.
661        * @param commandLine the commandline
662        */
663        public void defaults( final WriteableCommandLine commandLine )
664        {
665            super.defaults( commandLine );
666            for( final Iterator i = m_options.iterator(); i.hasNext();)
667            {
668                final Option option = (Option) i.next();
669                option.defaults( commandLine );
670            }
671    
672            for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
673            {
674                final Option option = (Option) i.next();
675                option.defaults( commandLine );
676            }
677        }
678    }
679    
680    /**
681    * A reverse string comparator.
682    */
683    final class ReverseStringComparator implements Comparator 
684    {
685        private static final Comparator INSTANCE = new ReverseStringComparator();
686    
687        private ReverseStringComparator() 
688        {
689            // static
690        }
691    
692        /**
693         * Gets a singleton instance of a ReverseStringComparator
694         * @return the singleton instance
695         */
696        public static final Comparator getInstance() 
697        {
698            return INSTANCE;
699        }
700    
701       /**
702        * Compare two instances.
703        * @param o1 the first instance
704        * @param o2 the second instance
705        * @return the result
706        */
707        public int compare( final Object o1, final Object o2 )
708        {
709            final String s1 = (String) o1;
710            final String s2 = (String) o2;
711            return -s1.compareTo( s2 );
712        }
713    }