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.Collections;
021import java.util.Comparator;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.List;
025import java.util.ListIterator;
026import java.util.Set;
027
028import net.dpml.cli.Argument;
029import net.dpml.cli.DisplaySetting;
030import net.dpml.cli.Group;
031import net.dpml.cli.OptionException;
032import net.dpml.cli.WriteableCommandLine;
033import net.dpml.cli.resource.ResourceConstants;
034
035/**
036 * A Parent implementation representing normal options.
037 *
038 * @author <a href="@PUBLISHER-URL@">@PUBLISHER-NAME@</a>
039 * @version @PROJECT-VERSION@
040 */
041public class DefaultOption extends ParentImpl
042{
043    /**
044     * The default token used to prefix a short option
045     */
046    public static final String DEFAULT_SHORT_PREFIX = "-";
047
048    /**
049     * The default token used to prefix a long option
050     */
051    public static final String DEFAULT_LONG_PREFIX = "--";
052
053    /**
054     * The default value for the burstEnabled constructor parameter
055     */
056    public static final boolean DEFAULT_BURST_ENABLED = true;
057    
058    private final String m_preferredName;
059    private final Set m_aliases;
060    private final Set m_burstAliases;
061    private final Set m_triggers;
062    private final Set m_prefixes;
063    private final String m_shortPrefix;
064    private final boolean m_burstEnabled;
065    private final int m_burstLength;
066
067    /**
068     * Creates a new DefaultOption
069     *
070     * @param shortPrefix the prefix used for short options
071     * @param longPrefix the prefix used for long options
072     * @param burstEnabled should option bursting be enabled
073     * @param preferredName the preferred name for this Option, this should 
074     *   begin with either shortPrefix or longPrefix
075     * @param description a description of this Option
076     * @param aliases the alternative names for this Option
077     * @param burstAliases the aliases that can be burst
078     * @param required whether the Option is strictly required
079     * @param argument the Argument belonging to this Parent, or null
080     * @param children the Group children belonging to this Parent, ot null
081     * @param id the unique identifier for this Option
082     * @throws IllegalArgumentException if the preferredName or an alias isn't
083     *     prefixed with shortPrefix or longPrefix
084     */
085    public DefaultOption(
086      final String shortPrefix, final String longPrefix, final boolean burstEnabled,
087      final String preferredName, final String description, final Set aliases,
088      final Set burstAliases, final boolean required, final Argument argument,
089      final Group children, final int id ) 
090      throws IllegalArgumentException
091    {
092        super( argument, children, description, id, required );
093
094        m_shortPrefix = shortPrefix;
095        m_burstEnabled = burstEnabled;
096        m_burstLength = shortPrefix.length() + 1;
097        m_preferredName = preferredName;
098        
099        if( aliases == null )
100        {
101            m_aliases = Collections.EMPTY_SET;
102        }
103        else
104        {
105            m_aliases = Collections.unmodifiableSet( new HashSet( aliases ) );
106        }
107        
108        if( burstAliases == null )
109        {
110            m_burstAliases = Collections.EMPTY_SET;
111        }
112        else
113        {
114            m_burstAliases = Collections.unmodifiableSet( new HashSet( burstAliases ) );
115        }
116        
117        final Set newTriggers = new HashSet();
118        newTriggers.add( m_preferredName );
119        newTriggers.addAll( m_aliases );
120        newTriggers.addAll( m_burstAliases );
121        m_triggers = Collections.unmodifiableSet( newTriggers );
122
123        final Set newPrefixes = new HashSet( super.getPrefixes() );
124        newPrefixes.add( m_shortPrefix );
125        newPrefixes.add( longPrefix );
126        m_prefixes = Collections.unmodifiableSet( newPrefixes );
127
128        checkPrefixes( newPrefixes );
129    }
130
131    /**
132     * Indicates whether this Option will be able to process the particular
133     * argument.
134     * 
135     * @param commandLine the CommandLine object to store defaults in
136     * @param argument the argument to be tested
137     * @return true if the argument can be processed by this Option
138     */
139    public boolean canProcess(
140      final WriteableCommandLine commandLine, final String argument )
141    {
142        return 
143          ( argument != null ) 
144          && ( 
145            super.canProcess( commandLine, argument ) 
146            || ( 
147              ( argument.length() >= m_burstLength ) 
148              && m_burstAliases.contains( 
149                argument.substring( 0, m_burstLength ) ) 
150            ) 
151          );
152    }
153
154    /**
155     * Process the parent.
156     * @param commandLine the CommandLine object to store defaults in
157     * @param arguments the ListIterator over String arguments
158     * @exception OptionException if an error occurs
159     */
160    public void processParent( WriteableCommandLine commandLine, ListIterator arguments )
161      throws OptionException 
162    {
163        final String argument = (String) arguments.next();
164
165        if( m_triggers.contains( argument ) )
166        {
167            commandLine.addOption( this );
168            arguments.set( m_preferredName );
169        } 
170        else if( m_burstEnabled && ( argument.length() >= m_burstLength ) )
171        {
172            final String burst = argument.substring( 0, m_burstLength );
173            if( m_burstAliases.contains( burst ) )
174            {
175                commandLine.addOption( this );
176                //HMM test bursting all vs bursting one by one.
177                arguments.set( m_preferredName );
178
179                if( getArgument() == null )
180                {
181                    arguments.add( m_shortPrefix + argument.substring( m_burstLength ) );
182                }
183                else
184                {
185                    arguments.add( argument.substring( m_burstLength ) );
186                }
187                arguments.previous();
188            } 
189            else
190            {
191                throw new OptionException(
192                  this, ResourceConstants.CANNOT_BURST, argument );
193            }
194        }
195        else
196        {
197            throw new OptionException(
198              this, 
199              ResourceConstants.UNEXPECTED_TOKEN,
200              argument );
201        }
202    }
203
204    /**
205     * Identifies the argument prefixes that should trigger this option. This
206     * is used to decide which of many Options should be tried when processing
207     * a given argument string.
208     * 
209     * The returned Set must not be null.
210     * 
211     * @return The set of triggers for this Option
212     */
213    public Set getTriggers()
214    {
215        return m_triggers;
216    }
217
218    /**
219     * Identifies the argument prefixes that should be considered options. This
220     * is used to identify whether a given string looks like an option or an
221     * argument value. Typically an option would return the set [--,-] while
222     * switches might offer [-,+].
223     * 
224     * The returned Set must not be null.
225     * 
226     * @return The set of prefixes for this Option
227     */
228    public Set getPrefixes() 
229    {
230        return m_prefixes;
231    }
232
233    /**
234     * Checks that the supplied CommandLine is valid with respect to this
235     * option.
236     * 
237     * @param commandLine the CommandLine to check.
238     * @throws OptionException if the CommandLine is not valid.
239     */
240    public void validate( WriteableCommandLine commandLine )
241      throws OptionException
242    {
243        if( isRequired() && !commandLine.hasOption( this ) )
244        {
245            throw new OptionException(
246              this,
247              ResourceConstants.OPTION_MISSING_REQUIRED,
248              getPreferredName() );
249        }
250        super.validate( commandLine );
251    }
252
253    /**
254     * Appends usage information to the specified StringBuffer
255     * 
256     * @param buffer the buffer to append to
257     * @param helpSettings a set of display settings @see DisplaySetting
258     * @param comp a comparator used to sort the Options
259     */
260    public void appendUsage(
261      final StringBuffer buffer, final Set helpSettings, final Comparator comp )
262    {
263        // do we display optionality
264        final boolean optional =
265          !isRequired() 
266          && helpSettings.contains( DisplaySetting.DISPLAY_OPTIONAL );
267          
268        final boolean displayAliases = 
269          helpSettings.contains( DisplaySetting.DISPLAY_ALIASES );
270
271        if( optional )
272        {
273            buffer.append( '[' );
274        }
275
276        buffer.append( m_preferredName );
277
278        if( displayAliases && !m_aliases.isEmpty() ) 
279        {
280            buffer.append( " (" );
281
282            final List list = new ArrayList( m_aliases );
283            Collections.sort( list );
284            for( final Iterator i = list.iterator(); i.hasNext();)
285            {
286                final String alias = (String) i.next();
287                buffer.append( alias );
288                if( i.hasNext() )
289                {
290                    buffer.append( ',' );
291                }
292            }
293            buffer.append( ')' );
294        }
295
296        super.appendUsage( buffer, helpSettings, comp );
297
298        if( optional )
299        {
300            buffer.append( ']' );
301        }
302    }
303
304    /**
305     * The preferred name of an option is used for generating help and usage
306     * information.
307     * 
308     * @return The preferred name of the option
309     */
310    public String getPreferredName()
311    {
312        return m_preferredName;
313    }
314}