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.ArrayList;
020    import java.util.Collections;
021    import java.util.Comparator;
022    import java.util.HashSet;
023    import java.util.Iterator;
024    import java.util.List;
025    import java.util.ListIterator;
026    import java.util.Set;
027    
028    import net.dpml.cli.Argument;
029    import net.dpml.cli.DisplaySetting;
030    import net.dpml.cli.Group;
031    import net.dpml.cli.OptionException;
032    import net.dpml.cli.WriteableCommandLine;
033    import net.dpml.cli.resource.ResourceConstants;
034    import 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="http://www.dpml.net">Digital Product Meta Library</a>
040     * @version 1.0.0
041     */
042    public 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    }