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.Collections;
020import java.util.Comparator;
021import java.util.List;
022import java.util.ListIterator;
023import java.util.Set;
024import java.util.StringTokenizer;
025
026import net.dpml.cli.Argument;
027import net.dpml.cli.DisplaySetting;
028import net.dpml.cli.HelpLine;
029import net.dpml.cli.Option;
030import net.dpml.cli.OptionException;
031import net.dpml.cli.WriteableCommandLine;
032import net.dpml.cli.resource.ResourceConstants;
033import net.dpml.cli.resource.ResourceHelper;
034import net.dpml.cli.validation.InvalidArgumentException;
035import net.dpml.cli.validation.Validator;
036
037/**
038 * An implementation of an Argument.
039 * @author <a href="@PUBLISHER-URL@">@PUBLISHER-NAME@</a>
040 * @version @PROJECT-VERSION@
041 */
042public class ArgumentImpl extends OptionImpl implements Argument 
043{
044    private static final char NUL = '\0';
045
046    /**
047     * The default value for the initial separator char.
048     */
049    public static final char DEFAULT_INITIAL_SEPARATOR = NUL;
050
051    /**
052     * The default value for the subsequent separator char.
053     */
054    public static final char DEFAULT_SUBSEQUENT_SEPARATOR = NUL;
055
056    /**
057     * The default token to indicate that remaining arguments should be consumed
058     * as values.
059     */
060    public static final String DEFAULT_CONSUME_REMAINING = "--";
061    
062    private final String m_name;
063    private final String m_description;
064    private final int m_minimum;
065    private final int m_maximum;
066    private final char m_initialSeparator;
067    private final char m_subsequentSeparator;
068    private final boolean m_subsequentSplit;
069    private final Validator m_validator;
070    private final String m_consumeRemaining;
071    private final List m_defaultValues;
072    private final ResourceHelper m_resources = ResourceHelper.getResourceHelper();
073
074    /**
075     * Creates a new Argument instance.
076     *
077     * @param name the name of the argument
078     * @param description a description of the argument
079     * @param minimum the minimum number of values needed to be valid
080     * @param maximum the maximum number of values allowed to be valid
081     * @param initialSeparator the char separating option from value
082     * @param subsequentSeparator the char separating values from each other
083     * @param validator object responsible for validating the values
084     * @param consumeRemaining String used for the "consuming option" group
085     * @param valueDefaults values to be used if none are specified.
086     * @param id the id of the option, 0 implies automatic assignment.
087     *
088     * @see OptionImpl#OptionImpl(int,boolean)
089     */
090    public ArgumentImpl(
091      final String name, final String description, final int minimum, final int maximum,
092      final char initialSeparator, final char subsequentSeparator, final Validator validator,
093      final String consumeRemaining, final List valueDefaults, final int id ) 
094    {
095        super( id, false );
096
097        m_description = description;
098        m_minimum = minimum;
099        m_maximum = maximum;
100        m_initialSeparator = initialSeparator;
101        m_subsequentSeparator = subsequentSeparator;
102        m_subsequentSplit = subsequentSeparator != NUL;
103        m_validator = validator;
104        m_consumeRemaining = consumeRemaining;
105        m_defaultValues = valueDefaults;
106
107        if( null == name )
108        {
109            m_name = "arg";
110        }
111        else
112        {
113            m_name = name;
114        }
115        
116        if( m_minimum > m_maximum )
117        {
118            throw new IllegalArgumentException(
119              m_resources.getMessage(
120                ResourceConstants.ARGUMENT_MIN_EXCEEDS_MAX ) );
121        }
122
123        if( ( m_defaultValues != null ) && ( m_defaultValues.size() > 0 ) )
124        {
125            if( valueDefaults.size() < minimum )
126            {
127                throw new IllegalArgumentException(
128                  m_resources.getMessage(
129                    ResourceConstants.ARGUMENT_TOO_FEW_DEFAULTS ) );
130            }
131            if( m_defaultValues.size() > maximum )
132            {
133                throw new IllegalArgumentException(
134                  m_resources.getMessage( 
135                    ResourceConstants.ARGUMENT_TOO_MANY_DEFAULTS ) );
136            }
137        }
138    }
139
140    /**
141     * The preferred name of an option is used for generating help and usage
142     * information.
143     * 
144     * @return The preferred name of the option
145     */
146    public String getPreferredName()
147    {
148        return m_name;
149    }
150    
151   /**
152    * Processes the "README" style element of the argument.
153    *
154    * Values identified should be added to the CommandLine object in
155    * association with this Argument.
156    *
157    * @see WriteableCommandLine#addValue(Option,Object)
158    *
159    * @param commandLine The CommandLine object to store results in.
160    * @param arguments The arguments to process.
161    * @param option The option to register value against.
162    * @throws OptionException if any problems occur.
163    */
164    public void processValues(
165      final WriteableCommandLine commandLine, final ListIterator arguments, final Option option )
166      throws OptionException
167    {
168        int argumentCount = commandLine.getValues( option, Collections.EMPTY_LIST ).size();
169
170        while( arguments.hasNext() && ( argumentCount < m_maximum ) )
171        {
172            final String allValues = stripBoundaryQuotes( (String) arguments.next() );
173
174            // should we ignore things that look like options?
175            if( allValues.equals( m_consumeRemaining ) )
176            {
177                while( arguments.hasNext() && ( argumentCount < m_maximum ) )
178                {
179                    ++argumentCount;
180                    commandLine.addValue( option, arguments.next() );
181                }
182            }
183            // does it look like an option?
184            else if( commandLine.looksLikeOption( allValues ) )
185            {
186                arguments.previous();
187                break;
188            }
189            // should we split the string up?
190            else if( m_subsequentSplit )
191            {
192                final StringTokenizer values =
193                  new StringTokenizer( allValues, String.valueOf( m_subsequentSeparator ) );
194                arguments.remove();
195
196                while( values.hasMoreTokens() && ( argumentCount < m_maximum ) )
197                {
198                    ++argumentCount;
199                    final String token = values.nextToken();
200                    commandLine.addValue( option, token );
201                    arguments.add( token );
202                }
203
204                if( values.hasMoreTokens() )
205                {
206                    throw new OptionException(
207                      option, 
208                      ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
209                      values.nextToken() );
210                }
211            }
212            else 
213            {
214                // it must be a value as it is
215                ++argumentCount;
216                commandLine.addValue( option, allValues );
217            }
218        }
219    }
220
221    /**
222     * Indicates whether this Option will be able to process the particular
223     * argument.
224     * 
225     * @param commandLine the CommandLine object to store defaults in
226     * @param argument the argument to be tested
227     * @return true if the argument can be processed by this Option
228     */
229    public boolean canProcess( final WriteableCommandLine commandLine, final String argument )
230    {
231        return true;
232    }
233
234    /**
235     * Identifies the argument prefixes that should be considered options. This
236     * is used to identify whether a given string looks like an option or an
237     * argument value. Typically an option would return the set [--,-] while
238     * switches might offer [-,+].
239     * 
240     * The returned Set must not be null.
241     * 
242     * @return The set of prefixes for this Option
243     */
244    public Set getPrefixes()
245    {
246        return Collections.EMPTY_SET;
247    }
248
249    /**
250     * Processes String arguments into a CommandLine.
251     * 
252     * The iterator will initially point at the first argument to be processed
253     * and at the end of the method should point to the first argument not
254     * processed. This method MUST process at least one argument from the
255     * ListIterator.
256     * 
257     * @param commandLine the CommandLine object to store results in
258     * @param args the arguments to process
259     * @throws OptionException if any problems occur
260     */
261    public void process( WriteableCommandLine commandLine, ListIterator args )
262      throws OptionException
263    {
264        processValues( commandLine, args, this );
265    }
266
267   /**
268    * Returns the initial separator character or
269    * '\0' if no character has been set.
270    * 
271    * @return char the initial separator character
272    */
273    public char getInitialSeparator()
274    {
275        return m_initialSeparator;
276    }
277
278   /**
279    * Returns the subsequent separator character.
280    * 
281    * @return the subsequent separator character
282    */
283    public char getSubsequentSeparator()
284    {
285        return m_subsequentSeparator;
286    }
287
288    /**
289     * Identifies the argument prefixes that should trigger this option. This
290     * is used to decide which of many Options should be tried when processing
291     * a given argument string.
292     * 
293     * The returned Set must not be null.
294     * 
295     * @return The set of triggers for this Option
296     */
297    public Set getTriggers()
298    {
299        return Collections.EMPTY_SET;
300    }
301
302   /**
303    * Return the consume remaining flag.
304    * @return the consume remaining flag
305    */
306    public String getConsumeRemaining()
307    {
308        return m_consumeRemaining;
309    }
310    
311   /**
312    * Return the list of default values.
313    * @return the default values
314    */
315    public List getDefaultValues() 
316    {
317        return m_defaultValues;
318    }
319    
320   /**
321    * Return the argument validator.
322    * @return the validator
323    */
324    public Validator getValidator()
325    {
326        return m_validator;
327    }
328    
329    /**
330     * Performs any necessary validation on the values added to the
331     * CommandLine.
332     *
333     * Validation will typically involve using the
334     * CommandLine.getValues(option) method to retrieve the values
335     * and then either checking each value.  Optionally the String
336     * value can be replaced by another Object such as a Number
337     * instance or a File instance.
338     *
339     * @see net.dpml.cli.CommandLine#getValues(Option)
340     *
341     * @param commandLine The CommandLine object to query.
342     * @throws OptionException if any problems occur.
343     */
344    public void validate( final WriteableCommandLine commandLine ) throws OptionException 
345    {
346        validate( commandLine, this );
347    }
348
349    /**
350     * Performs any necessary validation on the values added to the
351     * CommandLine.
352     *
353     * Validation will typically involve using the
354     * CommandLine.getValues(option) method to retrieve the values
355     * and then either checking each value.  Optionally the String
356     * value can be replaced by another Object such as a Number
357     * instance or a File instance.
358     *
359     * @see net.dpml.cli.CommandLine#getValues(Option)
360     *
361     * @param commandLine The CommandLine object to query.
362     * @param option The option to lookup values with.
363     * @throws OptionException if any problems occur.
364     */
365    public void validate(
366      final WriteableCommandLine commandLine, final Option option )
367      throws OptionException 
368    {
369        final List values = commandLine.getValues( option );
370        if( values.size() < m_minimum )
371        {
372            throw new OptionException(
373              option, 
374              ResourceConstants.ARGUMENT_MISSING_VALUES );
375        }
376
377        if( values.size() > m_maximum )
378        {
379            throw new OptionException(
380              option, 
381              ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
382              (String) values.get( m_maximum ) );
383        }
384
385        if( m_validator != null )
386        {
387            try 
388            {
389                m_validator.validate( values );
390            } 
391            catch( InvalidArgumentException ive )
392            {
393                throw new OptionException(
394                  option, 
395                  ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
396                  ive.getMessage() );
397            }
398        }
399    }
400
401    /**
402     * Appends usage information to the specified StringBuffer
403     * 
404     * @param buffer the buffer to append to
405     * @param helpSettings a set of display settings @see DisplaySetting
406     * @param comp a comparator used to sort the Options
407     */
408    public void appendUsage(
409      final StringBuffer buffer, final Set helpSettings, final Comparator comp )
410    {
411        // do we display the outer optionality
412        final boolean optional = helpSettings.contains( DisplaySetting.DISPLAY_OPTIONAL );
413
414        // allow numbering if multiple args
415        final boolean numbered =
416            ( m_maximum > 1 ) 
417            && helpSettings.contains( DisplaySetting.DISPLAY_ARGUMENT_NUMBERED );
418
419        final boolean bracketed = helpSettings.contains( DisplaySetting.DISPLAY_ARGUMENT_BRACKETED );
420
421        // if infinite args are allowed then crop the list
422        final int max = getMaxValue();
423        
424        int i = 0;
425
426        // for each argument
427        while( i < max )
428        {
429            // if we're past the first add a space
430            if( i > 0 )
431            {
432                buffer.append( ' ' );
433            }
434
435            // if the next arg is optional
436            if( ( i >= m_minimum ) && ( optional || ( i > 0 ) ) )
437            {
438                buffer.append( '[' );
439            }
440
441            if( bracketed )
442            {
443                buffer.append( '<' );
444            }
445
446            // add name
447            buffer.append( m_name );
448            ++i;
449
450            // if numbering
451            if( numbered )
452            {
453                buffer.append( i );
454            }
455
456            if( bracketed )
457            {
458                buffer.append( '>' );
459            }
460        }
461
462        // if infinite args are allowed
463        if( m_maximum == Integer.MAX_VALUE )
464        {
465            // append elipsis
466            buffer.append( " ..." );
467        }
468
469        // for each argument
470        while( i > 0 ) 
471        {
472            --i;
473            // if the next arg is optional
474            if( ( i >= m_minimum ) && ( optional || ( i > 0 ) ) )
475            {
476                buffer.append( ']' );
477            }
478        }
479    }
480    
481    /**
482     * Returns a description of the option. This string is used to build help
483     * messages as in the HelpFormatter.
484     * 
485     * @see net.dpml.cli.util.HelpFormatter
486     * @return a description of the option.
487     */
488    public String getDescription()
489    {
490        return m_description;
491    }
492
493    /**
494     * Builds up a list of HelpLineImpl instances to be presented by HelpFormatter.
495     * 
496     * @see HelpLine
497     * @see net.dpml.cli.util.HelpFormatter
498     * @param depth the initial indent depth
499     * @param helpSettings the HelpSettings that should be applied
500     * @param comp a comparator used to sort options when applicable.
501     * @return a List of HelpLineImpl objects
502     */
503    public List helpLines( final int depth, final Set helpSettings, final Comparator comp )
504    {
505        final HelpLine helpLine = new HelpLineImpl( this, depth );
506        return Collections.singletonList( helpLine );
507    }
508
509    /**
510     * Retrieves the maximum number of values acceptable for a valid Argument
511     *
512     * @return the maximum number of values
513     */
514    public int getMaximum()
515    {
516        return m_maximum;
517    }
518
519    /**
520     * Retrieves the minimum number of values required for a valid Argument
521     *
522     * @return the minimum number of values
523     */
524    public int getMinimum()
525    {
526        return m_minimum;
527    }
528
529    /**
530     * If there are any leading or trailing quotes remove them from the
531     * specified token.
532     *
533     * @param token the token to strip leading and trailing quotes
534     * @return String the possibly modified token
535     */
536    public String stripBoundaryQuotes( String token ) 
537    {
538        if( !token.startsWith( "\"" ) || !token.endsWith( "\"" ) )
539        {
540            return token;
541        }
542        token = token.substring( 1, token.length() - 1 );
543        return token;
544    }
545
546    /**
547     * Indicates whether argument values must be present for the CommandLine to
548     * be valid.
549     *
550     * @see #getMinimum()
551     * @see #getMaximum()
552     * @return true iff the CommandLine will be invalid without at least one 
553     *         value
554     */
555    public boolean isRequired()
556    {
557        return getMinimum() > 0;
558    }
559
560    /**
561     * Adds defaults to a CommandLine.
562     * 
563     * @param commandLine the CommandLine object to store defaults in.
564     */
565    public void defaults( final WriteableCommandLine commandLine )
566    {
567        super.defaults( commandLine );
568        defaultValues( commandLine, this );
569    }
570
571    /**
572     * Adds defaults to a CommandLine.
573     * 
574     * @param commandLine the CommandLine object to store defaults in.
575     * @param option the Option to store the defaults against.
576     */
577    public void defaultValues( final WriteableCommandLine commandLine, final Option option )
578    {
579        commandLine.setDefaultValues( option, m_defaultValues );
580    }
581
582    private int getMaxValue()
583    {
584        if( m_maximum == Integer.MAX_VALUE )
585        {
586            return 2;
587        }
588        else
589        {
590            return m_maximum;
591        }
592    }
593
594}