Source for net.dpml.cli.option.GroupImpl

   1: /*
   2:  * Copyright 2003-2005 The Apache Software Foundation
   3:  * Copyright 2005 Stephen McConnell
   4:  *
   5:  * Licensed under the Apache License, Version 2.0 (the "License");
   6:  * you may not use this file except in compliance with the License.
   7:  * You may obtain a copy of the License at
   8:  *
   9:  *     http://www.apache.org/licenses/LICENSE-2.0
  10:  *
  11:  * Unless required by applicable law or agreed to in writing, software
  12:  * distributed under the License is distributed on an "AS IS" BASIS,
  13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14:  * See the License for the specific language governing permissions and
  15:  * limitations under the License.
  16:  */
  17: package net.dpml.cli.option;
  18: 
  19: import java.util.ArrayList;
  20: import java.util.Collection;
  21: import java.util.Collections;
  22: import java.util.Comparator;
  23: import java.util.HashSet;
  24: import java.util.Iterator;
  25: import java.util.List;
  26: import java.util.ListIterator;
  27: import java.util.Map;
  28: import java.util.Set;
  29: import java.util.SortedMap;
  30: import java.util.TreeMap;
  31: 
  32: import net.dpml.cli.Argument;
  33: import net.dpml.cli.DisplaySetting;
  34: import net.dpml.cli.Group;
  35: import net.dpml.cli.HelpLine;
  36: import net.dpml.cli.Option;
  37: import net.dpml.cli.OptionException;
  38: import net.dpml.cli.WriteableCommandLine;
  39: import net.dpml.cli.resource.ResourceConstants;
  40: 
  41: /**
  42:  * An implementation of Group
  43:  * @author <a href="@PUBLISHER-URL@">@PUBLISHER-NAME@</a>
  44:  * @version @PROJECT-VERSION@
  45:  */
  46: public class GroupImpl extends OptionImpl implements Group 
  47: {
  48:     private final String m_name;
  49:     private final String m_description;
  50:     private final List m_options;
  51:     private final int m_minimum;
  52:     private final int m_maximum;
  53:     private final List m_anonymous;
  54:     private final SortedMap m_optionMap;
  55:     private final Set m_prefixes;
  56: 
  57:     /**
  58:      * Creates a new GroupImpl using the specified parameters.
  59:      *
  60:      * @param options the Options and Arguments that make up the Group
  61:      * @param name the name of this Group, or null
  62:      * @param description a description of this Group
  63:      * @param minimum the minimum number of Options for a valid CommandLine
  64:      * @param maximum the maximum number of Options for a valid CommandLine
  65:      */
  66:     public GroupImpl(
  67:       final List options, final String name, final String description,
  68:       final int minimum, final int maximum )
  69:     {
  70:         super( 0, false );
  71: 
  72:         m_name = name;
  73:         m_description = description;
  74:         m_minimum = minimum;
  75:         m_maximum = maximum;
  76: 
  77:         // store a copy of the options to be used by the 
  78:         // help methods
  79:         m_options = Collections.unmodifiableList( options );
  80: 
  81:         // m_anonymous Argument temporary storage
  82:         final List newAnonymous = new ArrayList();
  83: 
  84:         // map (key=trigger & value=Option) temporary storage
  85:         final SortedMap newOptionMap = new TreeMap( ReverseStringComparator.getInstance() );
  86: 
  87:         // prefixes temporary storage
  88:         final Set newPrefixes = new HashSet();
  89: 
  90:         // process the options
  91:         for( final Iterator i = options.iterator(); i.hasNext();)
  92:         {
  93:             final Option option = (Option) i.next();
  94:             if( option instanceof Argument ) 
  95:             {
  96:                 i.remove();
  97:                 newAnonymous.add( option );
  98:             } 
  99:             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: }