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.option;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.ListIterator;
027import java.util.Map;
028import java.util.Set;
029import java.util.SortedMap;
030import java.util.TreeMap;
031
032import net.dpml.cli.Argument;
033import net.dpml.cli.DisplaySetting;
034import net.dpml.cli.Group;
035import net.dpml.cli.HelpLine;
036import net.dpml.cli.Option;
037import net.dpml.cli.OptionException;
038import net.dpml.cli.WriteableCommandLine;
039import net.dpml.cli.resource.ResourceConstants;
040
041/**
042 * An implementation of Group
043 * @author <a href="@PUBLISHER-URL@">@PUBLISHER-NAME@</a>
044 * @version @PROJECT-VERSION@
045 */
046public 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*/
683final 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}