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.Collection;
021 import java.util.Collections;
022 import java.util.Comparator;
023 import java.util.HashSet;
024 import java.util.Iterator;
025 import java.util.List;
026 import java.util.ListIterator;
027 import java.util.Map;
028 import java.util.Set;
029 import java.util.SortedMap;
030 import java.util.TreeMap;
031
032 import net.dpml.cli.Argument;
033 import net.dpml.cli.DisplaySetting;
034 import net.dpml.cli.Group;
035 import net.dpml.cli.HelpLine;
036 import net.dpml.cli.Option;
037 import net.dpml.cli.OptionException;
038 import net.dpml.cli.WriteableCommandLine;
039 import net.dpml.cli.resource.ResourceConstants;
040
041 /**
042 * An implementation of Group
043 * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
044 * @version 1.0.0
045 */
046 public class GroupImpl extends OptionImpl implements Group
047 {
048 private final String m_name;
049 private final String m_description;
050 private final List m_options;
051 private final int m_minimum;
052 private final int m_maximum;
053 private final List m_anonymous;
054 private final SortedMap m_optionMap;
055 private final Set m_prefixes;
056
057 /**
058 * Creates a new GroupImpl using the specified parameters.
059 *
060 * @param options the Options and Arguments that make up the Group
061 * @param name the name of this Group, or null
062 * @param description a description of this Group
063 * @param minimum the minimum number of Options for a valid CommandLine
064 * @param maximum the maximum number of Options for a valid CommandLine
065 */
066 public GroupImpl(
067 final List options, final String name, final String description,
068 final int minimum, final int maximum )
069 {
070 super( 0, false );
071
072 m_name = name;
073 m_description = description;
074 m_minimum = minimum;
075 m_maximum = maximum;
076
077 // store a copy of the options to be used by the
078 // help methods
079 m_options = Collections.unmodifiableList( options );
080
081 // m_anonymous Argument temporary storage
082 final List newAnonymous = new ArrayList();
083
084 // map (key=trigger & value=Option) temporary storage
085 final SortedMap newOptionMap = new TreeMap( ReverseStringComparator.getInstance() );
086
087 // prefixes temporary storage
088 final Set newPrefixes = new HashSet();
089
090 // process the options
091 for( final Iterator i = options.iterator(); i.hasNext();)
092 {
093 final Option option = (Option) i.next();
094 if( option instanceof Argument )
095 {
096 i.remove();
097 newAnonymous.add( option );
098 }
099 else
100 {
101 final Set triggers = option.getTriggers();
102 for( Iterator j = triggers.iterator(); j.hasNext();)
103 {
104 newOptionMap.put( j.next(), option );
105 }
106 // store the prefixes
107 newPrefixes.addAll( option.getPrefixes() );
108 }
109 }
110
111 m_anonymous = Collections.unmodifiableList( newAnonymous );
112 m_optionMap = Collections.unmodifiableSortedMap( newOptionMap );
113 m_prefixes = Collections.unmodifiableSet( newPrefixes );
114 }
115
116 /**
117 * Indicates whether this Option will be able to process the particular
118 * argument.
119 *
120 * @param commandLine the CommandLine object to store defaults in
121 * @param arg the argument to be tested
122 * @return true if the argument can be processed by this Option
123 */
124 public boolean canProcess(
125 final WriteableCommandLine commandLine, final String arg )
126 {
127 if( arg == null )
128 {
129 return false;
130 }
131
132 // if arg does not require bursting
133 if( m_optionMap.containsKey( arg ) )
134 {
135 return true;
136 }
137
138 // filter
139 final Map tailMap = m_optionMap.tailMap( arg );
140
141 // check if bursting is required
142 for( final Iterator iter = tailMap.values().iterator(); iter.hasNext();)
143 {
144 final Option option = (Option) iter.next();
145 if( option.canProcess( commandLine, arg ) )
146 {
147 return true;
148 }
149 }
150
151 if( commandLine.looksLikeOption( arg ) )
152 {
153 return false;
154 }
155
156 // m_anonymous argument(s) means we can process it
157 if( m_anonymous.size() > 0 )
158 {
159 return true;
160 }
161
162 return false;
163 }
164
165 /**
166 * Identifies the argument prefixes that should be considered options. This
167 * is used to identify whether a given string looks like an option or an
168 * argument value. Typically an option would return the set [--,-] while
169 * switches might offer [-,+].
170 *
171 * The returned Set must not be null.
172 *
173 * @return The set of prefixes for this Option
174 */
175 public Set getPrefixes()
176 {
177 return m_prefixes;
178 }
179
180 /**
181 * Identifies the argument prefixes that should trigger this option. This
182 * is used to decide which of many Options should be tried when processing
183 * a given argument string.
184 *
185 * The returned Set must not be null.
186 *
187 * @return The set of triggers for this Option
188 */
189 public Set getTriggers()
190 {
191 return m_optionMap.keySet();
192 }
193
194 /**
195 * Processes String arguments into a CommandLine.
196 *
197 * The iterator will initially point at the first argument to be processed
198 * and at the end of the method should point to the first argument not
199 * processed. This method MUST process at least one argument from the
200 * ListIterator.
201 *
202 * @param commandLine the CommandLine object to store results in
203 * @param arguments the arguments to process
204 * @throws OptionException if any problems occur
205 */
206 public void process(
207 final WriteableCommandLine commandLine, final ListIterator arguments )
208 throws OptionException
209 {
210 String previous = null;
211
212 // [START process each command line token
213 while( arguments.hasNext() )
214 {
215 // grab the next argument
216 final String arg = (String) arguments.next();
217
218 // if we have just tried to process this instance
219 if( arg == previous )
220 {
221 // rollback and abort
222 arguments.previous();
223 break;
224 }
225
226 // remember last processed instance
227 previous = arg;
228
229 final Option opt = (Option) m_optionMap.get( arg );
230
231 // option found
232 if( opt != null )
233 {
234 arguments.previous();
235 opt.process( commandLine, arguments );
236 }
237 // [START option NOT found
238 else
239 {
240 // it might be an m_anonymous argument continue search
241 // [START argument may be m_anonymous
242 if( commandLine.looksLikeOption( arg ) )
243 {
244 // narrow the search
245 final Collection values = m_optionMap.tailMap( arg ).values();
246 boolean foundMemberOption = false;
247 for( Iterator i = values.iterator(); i.hasNext() && !foundMemberOption;)
248 {
249 final Option option = (Option) i.next();
250 if( option.canProcess( commandLine, arg ) )
251 {
252 foundMemberOption = true;
253 arguments.previous();
254 option.process( commandLine, arguments );
255 }
256 }
257
258 // back track and abort this group if necessary
259 if( !foundMemberOption )
260 {
261 arguments.previous();
262 return;
263 }
264
265 } // [END argument may be m_anonymous
266 // [START argument is NOT m_anonymous
267 else
268 {
269 // move iterator back, current value not used
270 arguments.previous();
271
272 // if there are no m_anonymous arguments then this group can't
273 // process the argument
274 if( m_anonymous.isEmpty() )
275 {
276 break;
277 }
278
279 // why do we iterate over all m_anonymous arguments?
280 // canProcess will always return true?
281 for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
282 {
283 final Argument argument = (Argument) i.next();
284 if( argument.canProcess( commandLine, arguments ) )
285 {
286 argument.process( commandLine, arguments );
287 }
288 }
289 } // [END argument is NOT m_anonymous
290 } // [END option NOT found
291 } // [END process each command line token
292 }
293
294 /**
295 * Checks that the supplied CommandLine is valid with respect to this
296 * option.
297 *
298 * @param commandLine the CommandLine to check.
299 * @throws OptionException if the CommandLine is not valid.
300 */
301 public void validate( final WriteableCommandLine commandLine ) throws OptionException
302 {
303 // number of options found
304 int present = 0;
305
306 // reference to first unexpected option
307 Option unexpected = null;
308
309 for( final Iterator i = m_options.iterator(); i.hasNext();)
310 {
311 final Option option = (Option) i.next();
312
313 // if the child option is required then validate it
314 if( option.isRequired() )
315 {
316 option.validate( commandLine );
317 }
318
319 if( option instanceof Group )
320 {
321 option.validate( commandLine );
322 }
323
324 // if the child option is present then validate it
325 if( commandLine.hasOption( option ) )
326 {
327 if( ++present > m_maximum )
328 {
329 unexpected = option;
330 break;
331 }
332 option.validate( commandLine );
333 }
334 }
335
336 // too many options
337 if( unexpected != null )
338 {
339 throw new OptionException(
340 this,
341 ResourceConstants.UNEXPECTED_TOKEN,
342 unexpected.getPreferredName() );
343 }
344
345 // too few option
346 if( present < m_minimum )
347 {
348 throw new OptionException(
349 this,
350 ResourceConstants.MISSING_OPTION );
351 }
352
353 // validate each m_anonymous argument
354 for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
355 {
356 final Option option = (Option) i.next();
357 option.validate( commandLine );
358 }
359 }
360
361 /**
362 * The preferred name of an option is used for generating help and usage
363 * information.
364 *
365 * @return The preferred name of the option
366 */
367 public String getPreferredName()
368 {
369 return m_name;
370 }
371
372 /**
373 * Returns a description of the option. This string is used to build help
374 * messages as in the HelpFormatter.
375 *
376 * @see net.dpml.cli.util.HelpFormatter
377 * @return a description of the option.
378 */
379 public String getDescription()
380 {
381 return m_description;
382 }
383
384 /**
385 * Appends usage information to the specified StringBuffer
386 *
387 * @param buffer the buffer to append to
388 * @param helpSettings a set of display settings @see DisplaySetting
389 * @param comp a comparator used to sort the Options
390 */
391 public void appendUsage(
392 final StringBuffer buffer, final Set helpSettings, final Comparator comp )
393 {
394 if( getMaximum() == 1 )
395 {
396 appendUsage( buffer, helpSettings, comp, "|" );
397 }
398 else
399 {
400 appendUsage( buffer, helpSettings, comp, " " );
401 }
402 }
403
404 /**
405 * Appends usage information to the specified StringBuffer
406 *
407 * @param buffer the buffer to append to
408 * @param helpSettings a set of display settings @see DisplaySetting
409 * @param comp a comparator used to sort the Options
410 * @param separator the String used to separate member Options
411 */
412 public void appendUsage(
413 final StringBuffer buffer, final Set helpSettings, final Comparator comp,
414 final String separator )
415 {
416 final Set helpSettingsCopy = new HashSet( helpSettings );
417
418 final boolean optional =
419 ( m_minimum == 0 )
420 && helpSettingsCopy.contains( DisplaySetting.DISPLAY_OPTIONAL );
421
422 final boolean expanded =
423 ( m_name == null )
424 || helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED );
425
426 final boolean named =
427 !expanded
428 || ( ( m_name != null ) && helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_NAME ) );
429
430 final boolean arguments =
431 helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_ARGUMENT );
432
433 final boolean outer =
434 helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_OUTER );
435
436 helpSettingsCopy.remove( DisplaySetting.DISPLAY_GROUP_OUTER );
437
438 final boolean both = named && expanded;
439
440 if( optional )
441 {
442 buffer.append( '[' );
443 }
444
445 if( named )
446 {
447 buffer.append( m_name );
448 }
449
450 if( both )
451 {
452 buffer.append( " (" );
453 }
454
455 if( expanded )
456 {
457 final Set childSettings;
458
459 if( !helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ) )
460 {
461 childSettings = DisplaySetting.NONE;
462 }
463 else
464 {
465 childSettings = new HashSet( helpSettingsCopy );
466 childSettings.remove( DisplaySetting.DISPLAY_OPTIONAL );
467 }
468
469 // grab a list of the group's options.
470 final List list;
471
472 if( comp == null )
473 {
474 // default to using the initial order
475 list = m_options;
476 }
477 else
478 {
479 // sort options if comparator is supplied
480 list = new ArrayList( m_options );
481 Collections.sort( list, comp );
482 }
483
484 // for each option.
485 for( final Iterator i = list.iterator(); i.hasNext();)
486 {
487 final Option option = (Option) i.next();
488
489 // append usage information
490 option.appendUsage( buffer, childSettings, comp );
491
492 // add separators as needed
493 if( i.hasNext() )
494 {
495 buffer.append( separator );
496 }
497 }
498 }
499
500 if( both )
501 {
502 buffer.append( ')' );
503 }
504
505 if( optional && outer )
506 {
507 buffer.append( ']' );
508 }
509
510 if( arguments )
511 {
512 for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
513 {
514 buffer.append( ' ' );
515 final Option option = (Option) i.next();
516 option.appendUsage( buffer, helpSettingsCopy, comp );
517 }
518 }
519
520 if( optional && !outer )
521 {
522 buffer.append( ']' );
523 }
524 }
525
526 /**
527 * Builds up a list of HelpLineImpl instances to be presented by HelpFormatter.
528 *
529 * @see HelpLine
530 * @see net.dpml.cli.util.HelpFormatter
531 * @param depth the initial indent depth
532 * @param helpSettings the HelpSettings that should be applied
533 * @param comp a comparator used to sort options when applicable.
534 * @return a List of HelpLineImpl objects
535 */
536 public List helpLines(
537 final int depth, final Set helpSettings, final Comparator comp )
538 {
539 final List helpLines = new ArrayList();
540
541 if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_NAME ) )
542 {
543 final HelpLine helpLine = new HelpLineImpl( this, depth );
544 helpLines.add( helpLine );
545 }
546
547 if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ) )
548 {
549 // grab a list of the group's options.
550 final List list;
551
552 if( comp == null )
553 {
554 // default to using the initial order
555 list = m_options;
556 }
557 else
558 {
559 // sort options if comparator is supplied
560 list = new ArrayList( m_options );
561 Collections.sort( list, comp );
562 }
563
564 // for each option
565 for( final Iterator i = list.iterator(); i.hasNext();)
566 {
567 final Option option = (Option) i.next();
568 helpLines.addAll( option.helpLines( depth + 1, helpSettings, comp ) );
569 }
570 }
571
572 if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_ARGUMENT ) )
573 {
574 for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
575 {
576 final Option option = (Option) i.next();
577 helpLines.addAll( option.helpLines( depth + 1, helpSettings, comp ) );
578 }
579 }
580
581 return helpLines;
582 }
583
584 /**
585 * Gets the member Options of thie Group.
586 * Note this does not include any Arguments
587 * @return only the non Argument Options of the Group
588 */
589 public List getOptions()
590 {
591 return m_options;
592 }
593
594 /**
595 * Gets the m_anonymous Arguments of this Group.
596 * @return the Argument options of this Group
597 */
598 public List getAnonymous()
599 {
600 return m_anonymous;
601 }
602
603 /**
604 * Recursively searches for an option with the supplied trigger.
605 *
606 * @param trigger the trigger to search for.
607 * @return the matching option or null.
608 */
609 public Option findOption( final String trigger )
610 {
611 final Iterator i = getOptions().iterator();
612
613 while( i.hasNext() )
614 {
615 final Option option = (Option) i.next();
616 final Option found = option.findOption( trigger );
617 if( found != null )
618 {
619 return found;
620 }
621 }
622 return null;
623 }
624
625 /**
626 * Retrieves the minimum number of values required for a valid Argument
627 *
628 * @return the minimum number of values
629 */
630 public int getMinimum()
631 {
632 return m_minimum;
633 }
634
635 /**
636 * Retrieves the maximum number of values acceptable for a valid Argument
637 *
638 * @return the maximum number of values
639 */
640 public int getMaximum()
641 {
642 return m_maximum;
643 }
644
645 /**
646 * Indicates whether argument values must be present for the CommandLine to
647 * be valid.
648 *
649 * @see #getMinimum()
650 * @see #getMaximum()
651 * @return true iff the CommandLine will be invalid without at least one
652 * value
653 */
654 public boolean isRequired()
655 {
656 return getMinimum() > 0;
657 }
658
659 /**
660 * Process defaults.
661 * @param commandLine the commandline
662 */
663 public void defaults( final WriteableCommandLine commandLine )
664 {
665 super.defaults( commandLine );
666 for( final Iterator i = m_options.iterator(); i.hasNext();)
667 {
668 final Option option = (Option) i.next();
669 option.defaults( commandLine );
670 }
671
672 for( final Iterator i = m_anonymous.iterator(); i.hasNext();)
673 {
674 final Option option = (Option) i.next();
675 option.defaults( commandLine );
676 }
677 }
678 }
679
680 /**
681 * A reverse string comparator.
682 */
683 final class ReverseStringComparator implements Comparator
684 {
685 private static final Comparator INSTANCE = new ReverseStringComparator();
686
687 private ReverseStringComparator()
688 {
689 // static
690 }
691
692 /**
693 * Gets a singleton instance of a ReverseStringComparator
694 * @return the singleton instance
695 */
696 public static final Comparator getInstance()
697 {
698 return INSTANCE;
699 }
700
701 /**
702 * Compare two instances.
703 * @param o1 the first instance
704 * @param o2 the second instance
705 * @return the result
706 */
707 public int compare( final Object o1, final Object o2 )
708 {
709 final String s1 = (String) o1;
710 final String s2 = (String) o2;
711 return -s1.compareTo( s2 );
712 }
713 }