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.Collections;
020    import java.util.Comparator;
021    import java.util.List;
022    import java.util.ListIterator;
023    import java.util.Set;
024    import java.util.StringTokenizer;
025    
026    import net.dpml.cli.Argument;
027    import net.dpml.cli.DisplaySetting;
028    import net.dpml.cli.HelpLine;
029    import net.dpml.cli.Option;
030    import net.dpml.cli.OptionException;
031    import net.dpml.cli.WriteableCommandLine;
032    import net.dpml.cli.resource.ResourceConstants;
033    import net.dpml.cli.resource.ResourceHelper;
034    import net.dpml.cli.validation.InvalidArgumentException;
035    import net.dpml.cli.validation.Validator;
036    
037    /**
038     * An implementation of an Argument.
039     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
040     * @version 1.0.0
041     */
042    public 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    }