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;
034import net.dpml.cli.resource.ResourceHelper;
035
036/**
037 * A Parent implementation representing normal switch options.
038 * For example: <code>+d|-d</code> or <code>--enable-x|--disable-x</code>.
039 * @author <a href="@PUBLISHER-URL@">@PUBLISHER-NAME@</a>
040 * @version @PROJECT-VERSION@
041 */
042public class Switch extends ParentImpl
043{
044    /** i18n */
045    public static final ResourceHelper RESOURCES = 
046      ResourceHelper.getResourceHelper();
047
048    /**
049     * The default prefix for enabled switches
050     */
051    public static final String DEFAULT_ENABLED_PREFIX = "+";
052
053    /**
054     * The default prefix for disabled switches
055     */
056    public static final String DEFAULT_DISABLED_PREFIX = "-";
057    
058    private final String m_enabledPrefix;
059    private final String m_disabledPrefix;
060    private final Set m_triggers;
061    private final String m_preferredName;
062    private final Set m_aliases;
063    private final Set m_prefixes;
064    private final Boolean m_defaultSwitch;
065
066    /**
067     * Creates a new Switch with the specified parameters
068     * @param enabledPrefix the prefix used for enabled switches
069     * @param disabledPrefix the prefix used for disabled switches
070     * @param preferredName the preferred name of the switch
071     * @param aliases the aliases by which the Switch is known
072     * @param description a description of the Switch
073     * @param required whether the Option is strictly required
074     * @param argument the Argument belonging to this Parent, or null
075     * @param children the Group children belonging to this Parent, ot null
076     * @param id the unique identifier for this Option
077     * @param switchDefault the switch default value
078     * @throws IllegalArgumentException if the preferredName or an alias isn't
079     *     prefixed with enabledPrefix or disabledPrefix
080     */
081    public Switch(
082      final String enabledPrefix, final String disabledPrefix, final String preferredName,
083      final Set aliases, final String description, final boolean required,
084      final Argument argument, final Group children, final int id, 
085      final Boolean switchDefault )
086      throws IllegalArgumentException
087    {
088        super( argument, children, description, id, required );
089
090        if( enabledPrefix == null )
091        {
092            throw new IllegalArgumentException(
093              RESOURCES.getMessage( 
094                ResourceConstants.SWITCH_NO_ENABLED_PREFIX ) );
095        }
096
097        if( disabledPrefix == null )
098        {
099            throw new IllegalArgumentException(
100              RESOURCES.getMessage( 
101                ResourceConstants.SWITCH_NO_DISABLED_PREFIX ) );
102        }
103
104        if( enabledPrefix.startsWith( disabledPrefix ) )
105        {
106            throw new IllegalArgumentException(
107              RESOURCES.getMessage( 
108                ResourceConstants.SWITCH_ENABLED_STARTS_WITH_DISABLED ) );
109        }
110
111        if( disabledPrefix.startsWith( enabledPrefix ) )
112        {
113            throw new IllegalArgumentException(
114              RESOURCES.getMessage( 
115                ResourceConstants.SWITCH_DISABLED_STARTWS_WITH_ENABLED ) );
116        }
117
118        m_enabledPrefix = enabledPrefix;
119        m_disabledPrefix = disabledPrefix;
120        m_preferredName = preferredName;
121
122        if( ( preferredName == null ) || ( preferredName.length() < 1 ) )
123        {
124            throw new IllegalArgumentException(
125              RESOURCES.getMessage(
126                ResourceConstants.SWITCH_PREFERRED_NAME_TOO_SHORT ) );
127        }
128
129        final Set newTriggers = new HashSet();
130        newTriggers.add( enabledPrefix + preferredName );
131        newTriggers.add( disabledPrefix + preferredName );
132        m_triggers = Collections.unmodifiableSet( newTriggers );
133
134        if( aliases == null )
135        {
136            m_aliases = Collections.EMPTY_SET;
137        } 
138        else
139        {
140            m_aliases = Collections.unmodifiableSet( new HashSet( aliases ) );
141
142            for( final Iterator i = aliases.iterator(); i.hasNext();)
143            {
144                final String alias = (String) i.next();
145                newTriggers.add( enabledPrefix + alias );
146                newTriggers.add( disabledPrefix + alias );
147            }
148        }
149
150        final Set newPrefixes = new HashSet( super.getPrefixes() );
151        newPrefixes.add( enabledPrefix );
152        newPrefixes.add( disabledPrefix );
153        m_prefixes = Collections.unmodifiableSet( newPrefixes );
154        m_defaultSwitch = switchDefault;
155        checkPrefixes( newPrefixes );
156    }
157
158    /**
159     * Processes the parent part of the Option.  The combination of parent,
160     * argument and children is handled by the process method.
161     * @see net.dpml.cli.Option#process(WriteableCommandLine, ListIterator)
162     * 
163     * @param commandLine the CommandLine to write results to
164     * @param arguments a ListIterator over argument strings positioned at the next
165     *             argument to process
166     * @throws OptionException if an error occurs while processing
167     */
168    public void processParent(
169      final WriteableCommandLine commandLine, final ListIterator arguments )
170      throws OptionException
171    {
172        final String arg = (String) arguments.next();
173
174        if( canProcess( commandLine, arg ) )
175        {
176            if( arg.startsWith( m_enabledPrefix ) )
177            {
178                commandLine.addSwitch( this, true );
179                arguments.set( m_enabledPrefix + m_preferredName );
180            }
181            if( arg.startsWith( m_disabledPrefix ) )
182            {
183                commandLine.addSwitch( this, false );
184                arguments.set( m_disabledPrefix + m_preferredName );
185            }
186        } 
187        else
188        {
189            throw new OptionException(
190              this, 
191              ResourceConstants.UNEXPECTED_TOKEN, 
192              arg );
193        }
194    }
195
196    /**
197     * Identifies the argument prefixes that should trigger this option. This
198     * is used to decide which of many Options should be tried when processing
199     * a given argument string.
200     * 
201     * The returned Set must not be null.
202     * 
203     * @return The set of triggers for this Option
204     */
205    public Set getTriggers() 
206    {
207        return m_triggers;
208    }
209
210    /**
211     * Identifies the argument prefixes that should be considered options. This
212     * is used to identify whether a given string looks like an option or an
213     * argument value. Typically an option would return the set [--,-] while
214     * switches might offer [-,+].
215     * 
216     * The returned Set must not be null.
217     * 
218     * @return The set of prefixes for this Option
219     */
220    public Set getPrefixes() 
221    {
222        return m_prefixes;
223    }
224
225    /**
226     * Checks that the supplied CommandLine is valid with respect to this
227     * option.
228     * 
229     * @param commandLine the CommandLine to check.
230     * @throws OptionException if the CommandLine is not valid.
231     */
232    public void validate( WriteableCommandLine commandLine )
233      throws OptionException
234    {
235        if( isRequired() && !commandLine.hasOption( this ) )
236        {
237            throw new OptionException(
238              this, 
239              ResourceConstants.OPTION_MISSING_REQUIRED,
240              getPreferredName() );
241        }
242        super.validate( commandLine );
243    }
244
245    /**
246     * Appends usage information to the specified StringBuffer
247     * 
248     * @param buffer the buffer to append to
249     * @param helpSettings a set of display settings @see DisplaySetting
250     * @param comp a comparator used to sort the Options
251     */
252    public void appendUsage(
253      final StringBuffer buffer, final Set helpSettings, final Comparator comp )
254    {
255        // do we display optionality
256        final boolean optional =
257          !isRequired() 
258          && helpSettings.contains( DisplaySetting.DISPLAY_OPTIONAL );
259          
260        final boolean displayAliases = 
261          helpSettings.contains( DisplaySetting.DISPLAY_ALIASES );
262        final boolean disabled = 
263          helpSettings.contains( DisplaySetting.DISPLAY_SWITCH_DISABLED );
264        final boolean enabled =
265            !disabled || helpSettings.contains( DisplaySetting.DISPLAY_SWITCH_ENABLED );
266        final boolean both = disabled && enabled;
267
268        if( optional )
269        {
270            buffer.append( '[' );
271        }
272
273        if( enabled )
274        {
275            buffer.append( m_enabledPrefix ).append( m_preferredName );
276        }
277
278        if( both )
279        {
280            buffer.append( '|' );
281        }
282
283        if( disabled )
284        {
285            buffer.append( m_disabledPrefix ).append( m_preferredName );
286        }
287
288        if( displayAliases && !m_aliases.isEmpty() )
289        {
290            buffer.append( " (" );
291
292            final List list = new ArrayList( m_aliases );
293            Collections.sort( list );
294            for( final Iterator i = list.iterator(); i.hasNext();)
295            {
296                final String alias = (String) i.next();
297
298                if( enabled )
299                {
300                    buffer.append( m_enabledPrefix ).append( alias );
301                }
302                
303                if( both )
304                {
305                    buffer.append( '|' );
306                }
307
308                if( disabled )
309                {
310                    buffer.append( m_disabledPrefix ).append( alias );
311                }
312
313                if( i.hasNext() )
314                {
315                    buffer.append( ',' );
316                }
317            }
318            
319            buffer.append( ')' );
320        }
321
322        super.appendUsage( buffer, helpSettings, comp );
323
324        if( optional )
325        {
326            buffer.append( ']' );
327        }
328    }
329
330    /**
331     * The preferred name of an option is used for generating help and usage
332     * information.
333     * 
334     * @return The preferred name of the option
335     */
336    public String getPreferredName()
337    {
338        return m_enabledPrefix + m_preferredName;
339    }
340
341    /**
342     * Adds defaults to a CommandLine.
343     * 
344     * Any defaults for this option are applied as well as the defaults for 
345     * any contained options
346     * 
347     * @param commandLine the CommandLine object to store defaults in
348     */
349    public void defaults( final WriteableCommandLine commandLine )
350    {
351        commandLine.setDefaultSwitch( this, m_defaultSwitch );
352    }
353}