1 /* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved. 2 * 3 * This program and the accompanying materials are made available under 4 * the terms of the Common Public License v1.0 which accompanies this distribution, 5 * and is available at http://www.eclipse.org/legal/cpl-v10.html 6 * 7 * $Id: OptsParser.java,v 1.1.1.1 2004/05/09 16:57:57 vlad_r Exp $ 8 */ 9 package com.vladium.util.args; 10 11 import java.io.CharArrayWriter; 12 import java.io.IOException; 13 import java.io.InputStream; 14 import java.io.InputStreamReader; 15 import java.io.PrintWriter; 16 import java.io.Reader; 17 import java.util.ArrayList; 18 import java.util.HashMap; 19 import java.util.HashSet; 20 import java.util.Iterator; 21 import java.util.List; 22 import java.util.Map; 23 import java.util.Set; 24 25 import com.vladium.util.IConstants; 26 import com.vladium.util.ResourceLoader; 27 28 // ---------------------------------------------------------------------------- 29 /** 30 * @author Vlad Roubtsov, (C) 2002 31 */ 32 final class OptsParser implements IOptsParser 33 { 34 // public: ................................................................ 35 36 // TODO: #-comments 37 // TODO: prefixing for error messages 38 // TODO: support var subst (main class name, etc) 39 // TODO: support short/full usage 40 // TODO: support marking opts as for displayable in full usage only 41 usage(final PrintWriter out, final int level, final int width)42 public synchronized void usage (final PrintWriter out, final int level, final int width) 43 { 44 // TODO: use width 45 // TODO: cache? 46 47 final String prefix = OPT_PREFIXES [CANONICAL_OPT_PREFIX]; 48 49 for (Iterator i = m_metadata.getOptDefs (); i.hasNext (); ) 50 { 51 final OptDef optdef = (OptDef) i.next (); 52 53 if ((level < 2) && optdef.isDetailedOnly ()) // skip detailed usage only options 54 continue; 55 56 final StringBuffer line = new StringBuffer (" "); 57 58 final String canonicalName = optdef.getCanonicalName (); 59 final boolean isPattern = optdef.isPattern (); 60 61 line.append (prefix); 62 line.append (canonicalName); 63 if (isPattern) line.append ('*'); 64 65 final String [] names = optdef.getNames (); 66 for (int n = 0; n < names.length; ++ n) 67 { 68 final String name = names [n]; 69 if (! name.equals (canonicalName)) 70 { 71 line.append (", "); 72 73 line.append (prefix); 74 line.append (name); 75 if (isPattern) line.append ('*'); 76 } 77 } 78 79 final String vmnemonic = optdef.getValueMnemonic (); 80 if (vmnemonic != null) 81 { 82 line.append (' '); 83 line.append (vmnemonic); 84 } 85 86 87 int padding = 16 - line.length (); 88 if (padding < 2) 89 { 90 // end the current line 91 out.println (line); 92 93 line.setLength (0); 94 for (int p = 0; p < 16; ++ p) line.append (' '); 95 } 96 else 97 { 98 for (int p = 0; p < padding; ++ p) line.append (' '); 99 } 100 101 if (optdef.isRequired ()) line.append ("{required} "); 102 line.append (optdef.getDescription ()); 103 104 out.println (line); 105 } 106 107 if (level < DETAILED_USAGE) 108 { 109 final OptDef usageOptDef = m_metadata.getUsageOptDef (); 110 if ((usageOptDef != null) && (usageOptDef.getNames () != null) && (usageOptDef.getNames ().length > 1)) 111 { 112 out.println (); 113 out.println (" {use '" + usageOptDef.getNames () [1] + "' option to see detailed usage help}"); 114 } 115 } 116 } 117 parse(final String [] args)118 public synchronized IOpts parse (final String [] args) 119 { 120 if (args == null) throw new IllegalArgumentException ("null input: args"); 121 122 final Opts opts = new Opts (); 123 124 { 125 final String [] nv = new String [2]; // out buffer for getOptNameAndValue() 126 final String [] pp = new String [1]; // out buffer for getOptDef() 127 128 // running state/current vars: 129 int state = STATE_OPT; 130 OptDef optdef = null; 131 Opt opt = null; 132 String value = null; 133 int valueCount = 0; 134 135 int a; 136 scan: for (a = 0; a < args.length; ) 137 { 138 final String av = args [a]; 139 if (av == null) throw new IllegalArgumentException ("null input: args[" + a + "]"); 140 141 //System.out.println ("[state: " + state + "] av = " + av); 142 143 switch (state) 144 { 145 case STATE_OPT: 146 { 147 if (isOpt (av, valueCount, optdef)) 148 { 149 // 'av' looks like an option: get its name and see if it 150 // is in the metadata 151 152 valueCount = 0; 153 154 getOptNameAndValue (av, nv); // this can leave nv[1] as null 155 156 // [assertion: nv [0] != null] 157 158 final String optName = nv [0]; // is not necessarily canonical 159 optdef = m_metadata.getOptDef (optName, pp); // pp [0] is always set by this 160 161 if (optdef == null) 162 { 163 // unknown option: 164 165 // TODO: coded messages? 166 opts.addError (formatMessage ("unknown option \'" + optName + "\'")); 167 168 state = STATE_ERROR; 169 } 170 else 171 { 172 // merge if necessary: 173 174 final String canonicalName = getOptCanonicalName (optName, optdef); 175 final String patternPrefix = pp [0]; 176 177 opt = opts.getOpt (canonicalName); 178 179 if (optdef.isMergeable ()) 180 { 181 if (opt == null) 182 { 183 opt = new Opt (optName, canonicalName, patternPrefix); 184 opts.addOpt (opt, optdef, optName); 185 } 186 } 187 else 188 { 189 if (opt == null) 190 { 191 opt = new Opt (optName, canonicalName, patternPrefix); 192 opts.addOpt (opt, optdef, optName); 193 } 194 else 195 { 196 opts.addError (formatMessage ("option \'" + optName + "\' cannot be specified more than once")); 197 198 state = STATE_ERROR; 199 } 200 } 201 202 value = nv [1]; 203 204 if (value == null) ++ a; 205 state = STATE_OPT_VALUE; 206 } 207 } 208 else 209 { 210 // not in STATE_OPT_VALUE and 'av' does not look 211 // like an option: the rest of args are free 212 213 state = STATE_FREE_ARGS; 214 } 215 } 216 break; 217 218 219 case STATE_OPT_VALUE: 220 { 221 // [assertion: opt != null and optdef != null] 222 223 if (value != null) 224 { 225 // value specified explicitly using the <name>separator<value> syntax: 226 // [don't shift a] 227 228 valueCount = 1; 229 230 final int [] cardinality = optdef.getValueCardinality (); 231 232 if (cardinality [1] < 1) 233 { 234 opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept values: \'" + value + "\'")); 235 236 state = STATE_ERROR; 237 } 238 else 239 { 240 ++ a; 241 opt.addValue (value); 242 } 243 } 244 else 245 { 246 value = args [a]; 247 248 final int [] cardinality = optdef.getValueCardinality (); 249 250 if (isOpt (value, valueCount, optdef)) 251 { 252 if (valueCount < cardinality [0]) 253 { 254 opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept fewer than " + cardinality [0] + " value(s)")); 255 256 state = STATE_ERROR; 257 } 258 else 259 state = STATE_OPT; 260 } 261 else 262 { 263 if (valueCount < cardinality [1]) 264 { 265 ++ valueCount; 266 ++ a; 267 opt.addValue (value); 268 } 269 else 270 { 271 // this check is redundant: 272 // if (valueCount < cardinality [0]) 273 // { 274 // opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept fewer than " + cardinality [0] + " value(s)")); 275 // 276 // state = STATE_ERROR; 277 // } 278 // else 279 state = STATE_FREE_ARGS; 280 } 281 } 282 } 283 284 value = null; 285 } 286 break; 287 288 289 case STATE_FREE_ARGS: 290 { 291 if (isOpt (args [a], valueCount, optdef)) 292 { 293 state = STATE_OPT; 294 } 295 else 296 { 297 opts.setFreeArgs (args, a); 298 break scan; 299 } 300 } 301 break; 302 303 304 case STATE_ERROR: 305 { 306 break scan; // TODO: could use the current value of 'a' for a better error message 307 } 308 309 } // end of switch 310 } 311 312 if (a == args.length) 313 { 314 if (opt != null) // validate the last option's min cardinality 315 { 316 final int [] cardinality = optdef.getValueCardinality (); 317 318 if (valueCount < cardinality [0]) 319 { 320 opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept fewer than " + cardinality [0] + " value(s)")); 321 } 322 } 323 else 324 { 325 opts.setFreeArgs (args, a); 326 } 327 } 328 329 } // end of 'args' parsing 330 331 332 final IOpt [] specified = opts.getOpts (); 333 if (specified != null) 334 { 335 // validation: all required parameters must be specified 336 337 final Set /* String(canonical name) */ required = new HashSet (); 338 required.addAll (m_metadata.getRequiredOpts ()); 339 340 for (int s = 0; s < specified.length; ++ s) 341 { 342 required.remove (specified [s].getCanonicalName ()); 343 } 344 345 if (! required.isEmpty ()) 346 { 347 for (Iterator i = required.iterator (); i.hasNext (); ) 348 { 349 opts.addError (formatMessage ("missing required option \'" + (String) i.next () + "\'")); 350 } 351 } 352 353 for (int s = 0; s < specified.length; ++ s) 354 { 355 final IOpt opt = specified [s]; 356 final OptDef optdef = m_metadata.getOptDef (opt.getCanonicalName (), null); 357 358 // // validation: value cardinality constraints 359 // 360 // final int [] cardinality = optdef.getValueCardinality (); 361 // if (opt.getValueCount () < cardinality [0]) 362 // opts.addError (formatMessage ("option \'" + opt.getName () + "\' must have at least " + cardinality [0] + " value(s)")); 363 // else if (opt.getValueCount () > cardinality [1]) 364 // opts.addError (formatMessage ("option \'" + opt.getName () + "\' must not have more than " + cardinality [1] + " value(s)")); 365 366 // validation: "requires" constraints 367 368 final String [] requires = optdef.getRequiresSet (); // not canonicalized 369 if (requires != null) 370 { 371 for (int r = 0; r < requires.length; ++ r) 372 { 373 if (opts.getOpt (requires [r]) == null) 374 opts.addError (formatMessage ("option \'" + opt.getName () + "\' requires option \'" + requires [r] + "\'")); 375 } 376 } 377 378 // validation: "not with" constraints 379 380 final String [] excludes = optdef.getExcludesSet (); // not canonicalized 381 if (excludes != null) 382 { 383 for (int x = 0; x < excludes.length; ++ x) 384 { 385 final Opt xopt = opts.getOpt (excludes [x]); 386 if (xopt != null) 387 opts.addError (formatMessage ("option \'" + opt.getName () + "\' cannot be used with option \'" + xopt.getName () + "\'")); 388 } 389 } 390 391 // side effect: determine if usage is requested 392 393 if (optdef.isUsage ()) 394 { 395 opts.setUsageRequested (opt.getName ().equals (opt.getCanonicalName ()) ? SHORT_USAGE : DETAILED_USAGE); 396 } 397 } 398 } 399 400 return opts; 401 } 402 getOptCanonicalName(final String n, final OptDef optdef)403 private static String getOptCanonicalName (final String n, final OptDef optdef) 404 { 405 if (optdef.isPattern ()) 406 { 407 final String canonicalPattern = optdef.getCanonicalName (); 408 final String [] patterns = optdef.getNames (); 409 410 for (int p = 0; p < patterns.length; ++ p) 411 { 412 final String pattern = patterns [p]; 413 414 if (n.startsWith (pattern)) 415 { 416 return canonicalPattern.concat (n.substring (pattern.length ())); 417 } 418 } 419 420 // this should never happen: 421 throw new IllegalStateException ("failed to detect pattern prefix for [" + n + "]"); 422 } 423 else 424 { 425 return optdef.getCanonicalName (); 426 } 427 } 428 429 /* 430 * ['optdef' can be null if no current opt def context has been established] 431 * 432 * pre: av != null 433 * input not validated 434 */ isOpt(final String av, final int valueCount, final OptDef optdef)435 private static boolean isOpt (final String av, final int valueCount, final OptDef optdef) 436 { 437 if (optdef != null) 438 { 439 // if the current optdef calls for more values, consume the next token 440 // as an op value greedily, without looking at its prefix: 441 442 final int [] cardinality = optdef.getValueCardinality (); 443 444 if (valueCount < cardinality [1]) return false; 445 } 446 447 // else check av's prefix: 448 449 for (int p = 0; p < OPT_PREFIXES.length; ++ p) 450 { 451 if (av.startsWith (OPT_PREFIXES [p])) 452 return (av.length () > OPT_PREFIXES [p].length ()); 453 } 454 455 return false; 456 } 457 458 /* 459 * pre: av != null and isOpt(av)=true 460 * input not validated 461 */ getOptNameAndValue(final String av, final String [] nv)462 private static void getOptNameAndValue (final String av, final String [] nv) 463 { 464 nv [0] = null; 465 nv [1] = null; 466 467 for (int p = 0; p < OPT_PREFIXES.length; ++ p) 468 { 469 if ((av.startsWith (OPT_PREFIXES [p])) && (av.length () > OPT_PREFIXES [p].length ())) 470 { 471 final String name = av.substring (OPT_PREFIXES [p].length ()); // with a possible value after a separator 472 473 char separator = 0; 474 int sindex = Integer.MAX_VALUE; 475 476 for (int s = 0; s < OPT_VALUE_SEPARATORS.length; ++ s) 477 { 478 final int index = name.indexOf (OPT_VALUE_SEPARATORS [s]); 479 if ((index > 0) && (index < sindex)) 480 { 481 separator = OPT_VALUE_SEPARATORS [s]; 482 sindex = index; 483 } 484 } 485 486 if (separator != 0) 487 { 488 nv [0] = name.substring (0, sindex); 489 nv [1] = name.substring (sindex + 1); 490 } 491 else 492 { 493 nv [0] = name; 494 } 495 496 return; 497 } 498 } 499 } 500 501 // protected: ............................................................. 502 503 // package: ............................................................... 504 505 506 static final class Opt implements IOptsParser.IOpt 507 { getName()508 public String getName () 509 { 510 return m_name; 511 } 512 getCanonicalName()513 public String getCanonicalName () 514 { 515 return m_canonicalName; 516 } 517 getValueCount()518 public int getValueCount () 519 { 520 if (m_values == null) return 0; 521 522 return m_values.size (); 523 } 524 getFirstValue()525 public String getFirstValue () 526 { 527 if (m_values == null) return null; 528 529 return (String) m_values.get (0); 530 } 531 getValues()532 public String [] getValues () 533 { 534 if (m_values == null) return IConstants.EMPTY_STRING_ARRAY; 535 536 final String [] result = new String [m_values.size ()]; 537 m_values.toArray (result); 538 539 return result; 540 } 541 getPatternPrefix()542 public String getPatternPrefix () 543 { 544 return m_patternPrefix; 545 } 546 toString()547 public String toString () 548 { 549 final StringBuffer s = new StringBuffer (m_name); 550 if (! m_canonicalName.equals (m_name)) s.append (" [" + m_canonicalName + "]"); 551 552 if (m_values != null) 553 { 554 s.append (": "); 555 s.append (m_values); 556 } 557 558 return s.toString (); 559 } 560 Opt(final String name, final String canonicalName, final String patternPrefix)561 Opt (final String name, final String canonicalName, final String patternPrefix) 562 { 563 m_name = name; 564 m_canonicalName = canonicalName; 565 m_patternPrefix = patternPrefix; 566 } 567 addValue(final String value)568 void addValue (final String value) 569 { 570 if (value == null) throw new IllegalArgumentException ("null input: value"); 571 572 if (m_values == null) m_values = new ArrayList (); 573 m_values.add (value); 574 } 575 576 577 private final String m_name, m_canonicalName, m_patternPrefix; 578 private ArrayList m_values; 579 580 } // end of nested class 581 582 583 static final class Opts implements IOptsParser.IOpts 584 { usageRequestLevel()585 public int usageRequestLevel () 586 { 587 return m_usageRequestLevel; 588 } 589 error(final PrintWriter out, final int width)590 public void error (final PrintWriter out, final int width) 591 { 592 // TODO: use width 593 if (hasErrors ()) 594 { 595 for (Iterator i = m_errors.iterator (); i.hasNext (); ) 596 { 597 out.println (i.next ()); 598 } 599 } 600 } 601 getFreeArgs()602 public String [] getFreeArgs () 603 { 604 if (hasErrors ()) 605 throw new IllegalStateException (errorsToString ()); 606 607 return m_freeArgs; 608 } 609 getOpts()610 public IOpt [] getOpts () 611 { 612 if (hasErrors ()) return null; 613 614 if (m_opts.isEmpty ()) 615 return EMPTY_OPT_ARRAY; 616 else 617 { 618 final IOpt [] result = new IOpt [m_opts.size ()]; 619 m_opts.toArray (result); 620 621 return result; 622 } 623 } 624 getOpts(final String pattern)625 public IOpt [] getOpts (final String pattern) 626 { 627 if (hasErrors ()) return null; 628 629 final List /* Opt */ patternOpts = (List) m_patternMap.get (pattern); 630 631 if ((patternOpts == null) || patternOpts.isEmpty ()) 632 return EMPTY_OPT_ARRAY; 633 else 634 { 635 final IOpt [] result = new IOpt [patternOpts.size ()]; 636 patternOpts.toArray (result); 637 638 return result; 639 } 640 } 641 642 hasArg(final String name)643 public boolean hasArg (final String name) 644 { 645 if (hasErrors ()) 646 throw new IllegalStateException (errorsToString ()); 647 648 return m_nameMap.containsKey (name); 649 } 650 Opts()651 Opts () 652 { 653 m_opts = new ArrayList (); 654 m_nameMap = new HashMap (); 655 m_patternMap = new HashMap (); 656 } 657 addOpt(final Opt opt, final OptDef optdef, final String occuranceName)658 void addOpt (final Opt opt, final OptDef optdef, final String occuranceName) 659 { 660 if (opt == null) throw new IllegalArgumentException ("null input: opt"); 661 if (optdef == null) throw new IllegalArgumentException ("null input: optdef"); 662 if (occuranceName == null) throw new IllegalArgumentException ("null input: occuranceName"); 663 664 // [name collisions detected elsewhere] 665 666 m_opts.add (opt); 667 668 final String [] names = optdef.getNames (); 669 final boolean isPattern = (opt.getPatternPrefix () != null); 670 671 if (isPattern) 672 { 673 final String unprefixedName = occuranceName.substring (opt.getPatternPrefix ().length ()); 674 675 for (int n = 0; n < names.length; ++ n) 676 { 677 m_nameMap.put (names [n].concat (unprefixedName), opt); 678 } 679 680 { 681 final String canonicalPattern = optdef.getCanonicalName (); 682 683 List patternList = (List) m_patternMap.get (canonicalPattern); 684 if (patternList == null) 685 { 686 patternList = new ArrayList (); 687 for (int n = 0; n < names.length; ++ n) 688 { 689 m_patternMap.put (names [n], patternList); 690 } 691 } 692 693 patternList.add (opt); 694 } 695 } 696 else 697 { 698 for (int n = 0; n < names.length; ++ n) 699 { 700 m_nameMap.put (names [n], opt); 701 } 702 } 703 } 704 getOpt(final String occuranceName)705 Opt getOpt (final String occuranceName) 706 { 707 if (occuranceName == null) throw new IllegalArgumentException ("null input: occuranceName"); 708 709 return (Opt) m_nameMap.get (occuranceName); 710 } 711 setFreeArgs(final String [] args, final int start)712 void setFreeArgs (final String [] args, final int start) 713 { 714 if (args == null) throw new IllegalArgumentException ("null input: args"); 715 if ((start < 0) || (start > args.length)) throw new IllegalArgumentException ("invalid start index: " + start); 716 717 m_freeArgs = new String [args.length - start]; 718 System.arraycopy (args, start, m_freeArgs, 0, m_freeArgs.length); 719 } 720 setUsageRequested(final int level)721 void setUsageRequested (final int level) 722 { 723 m_usageRequestLevel = level; 724 } 725 addError(final String msg)726 void addError (final String msg) 727 { 728 if (msg != null) 729 { 730 if (m_errors == null) m_errors = new ArrayList (); 731 732 m_errors.add (msg); 733 } 734 } 735 hasErrors()736 boolean hasErrors () 737 { 738 return (m_errors != null) && ! m_errors.isEmpty (); 739 } 740 errorsToString()741 String errorsToString () 742 { 743 if (! hasErrors ()) return "<no errors>"; 744 745 final CharArrayWriter caw = new CharArrayWriter (); 746 final PrintWriter pw = new PrintWriter (caw); 747 748 error (pw, DEFAULT_ERROR_WIDTH); 749 pw.flush (); 750 751 return caw.toString (); 752 } 753 754 755 private final List /* Opt */ m_opts; 756 private final Map /* String(name/pattern-prefixed name)->Opt */ m_nameMap; 757 private final Map /* String(pattern prefix)->List<Opt> */ m_patternMap; 758 private String [] m_freeArgs; 759 private List /* String */ m_errors; 760 private int m_usageRequestLevel; 761 762 private static final int DEFAULT_ERROR_WIDTH = 80; 763 private static final IOpt [] EMPTY_OPT_ARRAY = new IOpt [0]; 764 765 } // end of nested class 766 767 768 static final class OptDef // TODO: merge with Opt? 769 { OptDef(final boolean usage)770 OptDef (final boolean usage) 771 { 772 m_usage = usage; 773 } 774 isUsage()775 boolean isUsage () 776 { 777 return m_usage; 778 } 779 getCanonicalName()780 String getCanonicalName () 781 { 782 return m_names [0]; 783 } 784 getNames()785 String [] getNames () 786 { 787 return m_names; 788 } 789 isRequired()790 boolean isRequired () 791 { 792 return m_required; 793 } 794 getValueMnemonic()795 String getValueMnemonic () 796 { 797 return m_valueMnemonic; 798 } 799 isMergeable()800 boolean isMergeable () 801 { 802 return m_mergeable; 803 } 804 isDetailedOnly()805 boolean isDetailedOnly () 806 { 807 return m_detailedOnly; 808 } 809 isPattern()810 boolean isPattern () 811 { 812 return m_pattern; 813 } 814 getValueCardinality()815 int [] getValueCardinality () 816 { 817 return m_valueCardinality; 818 } 819 getRequiresSet()820 String [] getRequiresSet () 821 { 822 return m_requiresSet; 823 } 824 getExcludesSet()825 String [] getExcludesSet () 826 { 827 return m_excludesSet; 828 } 829 getDescription()830 String getDescription () 831 { 832 return m_description; 833 } 834 setNames(final String [] names)835 void setNames (final String [] names) 836 { 837 if (names == null) throw new IllegalArgumentException ("null input: names"); 838 839 m_names = names; 840 } 841 setRequired(final boolean required)842 void setRequired (final boolean required) 843 { 844 m_required = required; 845 } 846 setValueMnemonic(final String mnemonic)847 void setValueMnemonic (final String mnemonic) 848 { 849 if (mnemonic == null) throw new IllegalArgumentException ("null input: mnemonic"); 850 851 m_valueMnemonic = mnemonic; 852 } 853 setMergeable(final boolean mergeable)854 void setMergeable (final boolean mergeable) 855 { 856 m_mergeable = mergeable; 857 } 858 setDetailedOnly(final boolean detailedOnly)859 void setDetailedOnly (final boolean detailedOnly) 860 { 861 m_detailedOnly = detailedOnly; 862 } 863 setPattern(final boolean pattern)864 void setPattern (final boolean pattern) 865 { 866 m_pattern = pattern; 867 } 868 setValueCardinality(final int [] cardinality)869 void setValueCardinality (final int [] cardinality) 870 { 871 if ((cardinality == null) || (cardinality.length != 2)) throw new IllegalArgumentException ("null or invalid input: cardinality"); 872 873 m_valueCardinality = cardinality; 874 } 875 setRequiresSet(final String [] names)876 void setRequiresSet (final String [] names) 877 { 878 if (names == null) throw new IllegalArgumentException ("null input: names"); 879 880 m_requiresSet = names.length > 0 ? names : null; 881 } 882 setExcludesSet(final String [] names)883 void setExcludesSet (final String [] names) 884 { 885 if (names == null) throw new IllegalArgumentException ("null input: names"); 886 887 m_excludesSet = names.length > 0 ? names : null; 888 } 889 setDescription(final String description)890 void setDescription (final String description) 891 { 892 if (description == null) throw new IllegalArgumentException ("null input: description"); 893 894 m_description = description; 895 } 896 897 898 static final int [] C_ZERO = new int [] {0, 0}; 899 static final int [] C_ONE = new int [] {1, 1}; 900 static final int [] C_ZERO_OR_ONE = new int [] {0, 1}; 901 static final int [] C_ZERO_OR_MORE = new int [] {0, Integer.MAX_VALUE}; 902 static final int [] C_ONE_OR_MORE = new int [] {1, Integer.MAX_VALUE}; 903 904 905 private final boolean m_usage; 906 private String [] m_names; 907 private boolean m_required; 908 private String m_valueMnemonic; 909 private boolean m_mergeable; 910 private boolean m_detailedOnly; 911 private boolean m_pattern; 912 private int [] m_valueCardinality; 913 private String [] m_requiresSet, m_excludesSet; 914 private String m_description; 915 916 } // end of nested class 917 918 919 static final class OptDefMetadata 920 { OptDefMetadata()921 OptDefMetadata () 922 { 923 m_optdefs = new ArrayList (); 924 m_optdefMap = new HashMap (); 925 m_requiredOpts = new HashSet (); 926 m_patternOptDefMap = new HashMap (); 927 } 928 getOptDef(final String name, final String [] prefixout)929 OptDef getOptDef (final String name, final String [] prefixout) 930 { 931 if (name == null) throw new IllegalArgumentException ("null input: name"); 932 933 if (prefixout != null) prefixout [0] = null; 934 935 // first, see if this is a regular option: 936 OptDef result = (OptDef) m_optdefMap.get (name); 937 938 // next, see if this is a prefixed option: 939 if (result == null) 940 { 941 for (Iterator ps = m_patternOptDefMap.entrySet ().iterator (); 942 ps.hasNext (); ) 943 { 944 final Map.Entry entry = (Map.Entry) ps.next (); 945 final String pattern = (String) entry.getKey (); 946 947 if (name.startsWith (pattern)) 948 { 949 if (prefixout != null) prefixout [0] = pattern; 950 result = (OptDef) entry.getValue (); 951 break; 952 } 953 } 954 } 955 956 return result; 957 } 958 getOptDefs()959 Iterator /* OptDef */ getOptDefs () 960 { 961 return m_optdefs.iterator (); 962 } 963 getPatternOptDefs(final String pattern)964 OptDef getPatternOptDefs (final String pattern) // returns null if no such pattern is defined 965 { 966 if (pattern == null) throw new IllegalArgumentException ("null input: pattern"); 967 968 return (OptDef) m_patternOptDefMap.get (pattern); 969 } 970 getRequiredOpts()971 Set /* String(canonical name) */ getRequiredOpts () 972 { 973 return m_requiredOpts; 974 } 975 getUsageOptDef()976 OptDef getUsageOptDef () 977 { 978 return m_usageOptDef; 979 } 980 addOptDef(final OptDef optdef)981 void addOptDef (final OptDef optdef) 982 { 983 if (optdef == null) throw new IllegalArgumentException ("null input: optdef"); 984 985 final Map map = optdef.isPattern () ? m_patternOptDefMap : m_optdefMap; 986 final String [] names = optdef.getNames (); 987 988 for (int n = 0; n < names.length; ++ n) 989 { 990 if (map.containsKey (names [n])) 991 throw new IllegalArgumentException ("duplicate option name [" + names [n] + "]"); 992 993 map.put (names [n], optdef); 994 } 995 996 m_optdefs.add (optdef); 997 998 if (optdef.isRequired ()) 999 m_requiredOpts.add (optdef.getCanonicalName ()); 1000 1001 if (optdef.isUsage ()) 1002 { 1003 if (m_usageOptDef != null) 1004 throw new IllegalArgumentException ("usage optdef set already"); 1005 1006 m_usageOptDef = optdef; 1007 } 1008 } 1009 1010 1011 final List /* OptDef */ m_optdefs; // keeps the addition order 1012 final Map /* String(name)->OptDef */ m_optdefMap; 1013 final Set /* String(canonical name) */ m_requiredOpts; 1014 final Map /* String(pattern name)->OptDef */ m_patternOptDefMap; 1015 private OptDef m_usageOptDef; 1016 1017 } // end of nested class 1018 1019 1020 static final class MetadataParser 1021 { 1022 /* 1023 * metadata := ( optdef )* <EOF> 1024 * 1025 * optdef := optnamelist ":" optmetadata ";" 1026 * optnamelist := namelist 1027 * optmetadata := 1028 * ("optional" | "required" ) 1029 * [ "," "mergeable" ] 1030 * [ "," "detailedonly" ] 1031 * [ "," "pattern" ] 1032 * "," "values" ":" cardinality 1033 * [ "," name ] 1034 * [ "," "requires" "{" namelist "}" ] 1035 * [ "," "notwith" "{" namelist "}" ] 1036 * "," text 1037 * cardinality := "0" | "1" | "?" 1038 * namelist := name ( "," name )* 1039 * name := <single quoted string> 1040 * text := <double quoted string> 1041 */ parse(final Reader in)1042 OptDef [] parse (final Reader in) 1043 { 1044 if (in == null) throw new IllegalArgumentException ("null input: in"); 1045 m_in = in; 1046 1047 nextChar (); 1048 nextToken (); 1049 1050 while (m_token != Token.EOF) 1051 { 1052 if (m_opts == null) m_opts = new ArrayList (); 1053 m_opts.add (optdef ()); 1054 } 1055 1056 final OptDef [] result; 1057 1058 if ((m_opts == null) || (m_opts.size () == 0)) 1059 result = EMPTY_OPTDEF_ARRAY; 1060 else 1061 { 1062 result = new OptDef [m_opts.size ()]; 1063 m_opts.toArray (result); 1064 } 1065 1066 m_in = null; 1067 m_opts = null; 1068 1069 return result; 1070 } 1071 optdef()1072 OptDef optdef () 1073 { 1074 final OptDef optdef = new OptDef (false); 1075 1076 optdef.setNames (optnamelist ()); 1077 accept (Token.COLON_ID); 1078 optmetadata (optdef); 1079 accept (Token.SEMICOLON_ID); 1080 1081 return optdef; 1082 } 1083 optnamelist()1084 String [] optnamelist () 1085 { 1086 return namelist (); 1087 } 1088 optmetadata(final OptDef optdef)1089 void optmetadata (final OptDef optdef) 1090 { 1091 switch (m_token.getID ()) 1092 { 1093 case Token.REQUIRED_ID: 1094 { 1095 accept (); 1096 optdef.setRequired (true); 1097 } 1098 break; 1099 1100 case Token.OPTIONAL_ID: 1101 { 1102 accept (); 1103 optdef.setRequired (false); 1104 } 1105 break; 1106 1107 default: 1108 throw new IllegalArgumentException ("parse error: invalid token " + m_token + ", expected " + Token.REQUIRED + " or " + Token.OPTIONAL); 1109 1110 } // end of switch 1111 1112 accept (Token.COMMA_ID); 1113 1114 if (m_token.getID () == Token.MERGEABLE_ID) 1115 { 1116 accept (); 1117 optdef.setMergeable (true); 1118 1119 accept (Token.COMMA_ID); 1120 } 1121 1122 if (m_token.getID () == Token.DETAILEDONLY_ID) 1123 { 1124 accept (); 1125 optdef.setDetailedOnly (true); 1126 1127 accept (Token.COMMA_ID); 1128 } 1129 1130 if (m_token.getID () == Token.PATTERN_ID) 1131 { 1132 accept (); 1133 optdef.setPattern (true); 1134 1135 accept (Token.COMMA_ID); 1136 } 1137 1138 accept (Token.VALUES_ID); 1139 accept (Token.COLON_ID); 1140 optdef.setValueCardinality (cardinality ()); 1141 1142 accept (Token.COMMA_ID); 1143 if (m_token.getID () == Token.STRING_ID) 1144 { 1145 optdef.setValueMnemonic (m_token.getValue ()); 1146 accept (); 1147 1148 accept (Token.COMMA_ID); 1149 } 1150 1151 if (m_token.getID () == Token.REQUIRES_ID) 1152 { 1153 accept (); 1154 1155 accept (Token.LBRACKET_ID); 1156 optdef.setRequiresSet (namelist ()); 1157 accept (Token.RBRACKET_ID); 1158 1159 accept (Token.COMMA_ID); 1160 } 1161 1162 if (m_token.getID () == Token.EXCLUDES_ID) 1163 { 1164 accept (); 1165 1166 accept (Token.LBRACKET_ID); 1167 optdef.setExcludesSet (namelist ()); 1168 accept (Token.RBRACKET_ID); 1169 1170 accept (Token.COMMA_ID); 1171 } 1172 1173 optdef.setDescription (accept (Token.TEXT_ID).getValue ()); 1174 } 1175 cardinality()1176 int [] cardinality () 1177 { 1178 final Token result = accept (Token.CARD_ID); 1179 1180 if ("0".equals (result.getValue ())) 1181 return OptDef.C_ZERO; 1182 else if ("1".equals (result.getValue ())) 1183 return OptDef.C_ONE; 1184 else // ? 1185 return OptDef.C_ZERO_OR_ONE; 1186 } 1187 namelist()1188 String [] namelist () 1189 { 1190 final List _result = new ArrayList (); 1191 1192 _result.add (accept (Token.STRING_ID).getValue ()); 1193 while (m_token.getID () == Token.COMMA_ID) 1194 { 1195 accept (); 1196 _result.add (accept (Token.STRING_ID).getValue ()); 1197 } 1198 1199 final String [] result = new String [_result.size ()]; 1200 _result.toArray (result); 1201 1202 return result; 1203 } 1204 1205 accept()1206 Token accept () 1207 { 1208 final Token current = m_token; 1209 nextToken (); 1210 1211 return current; 1212 } 1213 accept(final int tokenID)1214 Token accept (final int tokenID) 1215 { 1216 final Token current = m_token; 1217 1218 if (m_token.getID () == tokenID) 1219 nextToken (); 1220 else 1221 throw new IllegalArgumentException ("parse error: invalid token [" + m_token + "], expected type [" + tokenID + "]"); 1222 1223 return current; 1224 } 1225 1226 // "scanner": 1227 nextToken()1228 void nextToken () 1229 { 1230 consumeWS (); 1231 1232 switch (m_currentChar) 1233 { 1234 case -1: m_token = Token.EOF; break; 1235 1236 case ':': 1237 { 1238 nextChar (); 1239 m_token = Token.COLON; 1240 } 1241 break; 1242 1243 case ';': 1244 { 1245 nextChar (); 1246 m_token = Token.SEMICOLON; 1247 } 1248 break; 1249 1250 case ',': 1251 { 1252 nextChar (); 1253 m_token = Token.COMMA; 1254 } 1255 break; 1256 1257 case '{': 1258 { 1259 nextChar (); 1260 m_token = Token.LBRACKET; 1261 } 1262 break; 1263 1264 case '}': 1265 { 1266 nextChar (); 1267 m_token = Token.RBRACKET; 1268 } 1269 break; 1270 1271 case '0': 1272 { 1273 nextChar (); 1274 m_token = new Token (Token.CARD_ID, "0"); 1275 } 1276 break; 1277 1278 case '1': 1279 { 1280 nextChar (); 1281 m_token = new Token (Token.CARD_ID, "1"); 1282 } 1283 break; 1284 1285 case '?': 1286 { 1287 nextChar (); 1288 m_token = new Token (Token.CARD_ID, "?"); 1289 } 1290 break; 1291 1292 case '\'': 1293 { 1294 final StringBuffer value = new StringBuffer (); 1295 1296 nextChar (); 1297 while (m_currentChar != '\'') 1298 { 1299 value.append ((char) m_currentChar); 1300 nextChar (); 1301 } 1302 nextChar (); 1303 1304 m_token = new Token (Token.STRING_ID, value.toString ()); 1305 } 1306 break; 1307 1308 case '\"': 1309 { 1310 final StringBuffer value = new StringBuffer (); 1311 1312 nextChar (); 1313 while (m_currentChar != '\"') 1314 { 1315 value.append ((char) m_currentChar); 1316 nextChar (); 1317 } 1318 nextChar (); 1319 1320 m_token = new Token (Token.TEXT_ID, value.toString ()); 1321 } 1322 break; 1323 1324 default: 1325 { 1326 final StringBuffer value = new StringBuffer (); 1327 1328 while (Character.isLetter ((char) m_currentChar)) 1329 { 1330 value.append ((char) m_currentChar); 1331 nextChar (); 1332 } 1333 1334 final Token token = (Token) KEYWORDS.get (value.toString ()); 1335 if (token == null) 1336 throw new IllegalArgumentException ("parse error: unrecognized keyword [" + value + "]"); 1337 1338 m_token = token; 1339 } 1340 1341 } // end of switch 1342 } 1343 1344 consumeWS()1345 private void consumeWS () 1346 { 1347 if (m_currentChar == -1) 1348 return; 1349 else 1350 { 1351 while (Character.isWhitespace ((char) m_currentChar)) 1352 { 1353 nextChar (); 1354 } 1355 } 1356 1357 // TODO: #-comments 1358 } 1359 nextChar()1360 private void nextChar () 1361 { 1362 try 1363 { 1364 m_currentChar = m_in.read (); 1365 } 1366 catch (IOException ioe) 1367 { 1368 throw new RuntimeException ("I/O error while parsing: " + ioe); 1369 } 1370 } 1371 1372 1373 private Reader m_in; 1374 private List m_opts; 1375 1376 private Token m_token; 1377 private int m_currentChar; 1378 1379 private static final Map KEYWORDS; 1380 1381 private static final OptDef [] EMPTY_OPTDEF_ARRAY = new OptDef [0]; 1382 1383 static 1384 { 1385 KEYWORDS = new HashMap (17); 1386 Token.OPTIONAL.getValue()1387 KEYWORDS.put (Token.OPTIONAL.getValue (), Token.OPTIONAL); Token.REQUIRED.getValue()1388 KEYWORDS.put (Token.REQUIRED.getValue (), Token.REQUIRED); Token.VALUES.getValue()1389 KEYWORDS.put (Token.VALUES.getValue (), Token.VALUES); Token.REQUIRES.getValue()1390 KEYWORDS.put (Token.REQUIRES.getValue (), Token.REQUIRES); Token.EXCLUDES.getValue()1391 KEYWORDS.put (Token.EXCLUDES.getValue (), Token.EXCLUDES); Token.MERGEABLE.getValue()1392 KEYWORDS.put (Token.MERGEABLE.getValue (), Token.MERGEABLE); Token.DETAILEDONLY.getValue()1393 KEYWORDS.put (Token.DETAILEDONLY.getValue (), Token.DETAILEDONLY); Token.PATTERN.getValue()1394 KEYWORDS.put (Token.PATTERN.getValue (), Token.PATTERN); 1395 } 1396 1397 } // end of nested class 1398 1399 OptsParser(final String metadataResourceName, final ClassLoader loader, final String [] usageOpts)1400 OptsParser (final String metadataResourceName, final ClassLoader loader, final String [] usageOpts) 1401 { 1402 this (metadataResourceName, loader, null, usageOpts); 1403 } 1404 OptsParser(final String metadataResourceName, final ClassLoader loader, final String msgPrefix, final String [] usageOpts)1405 OptsParser (final String metadataResourceName, final ClassLoader loader, final String msgPrefix, final String [] usageOpts) 1406 { 1407 if (metadataResourceName == null) throw new IllegalArgumentException ("null input: metadataResourceName"); 1408 1409 m_msgPrefix = msgPrefix; 1410 1411 InputStream in = null; 1412 try 1413 { 1414 in = ResourceLoader.getResourceAsStream (metadataResourceName, loader); 1415 if (in == null) 1416 throw new IllegalArgumentException ("resource [" + metadataResourceName + "] could not be loaded via [" + loader + "]"); 1417 1418 // TODO: encoding 1419 final Reader rin = new InputStreamReader (in); 1420 1421 m_metadata = parseOptDefMetadata (rin, usageOpts); 1422 } 1423 finally 1424 { 1425 if (in != null) try { in.close (); } catch (IOException ignore) {} 1426 } 1427 } 1428 1429 // private: ............................................................... 1430 1431 1432 private static final class Token 1433 { Token(final int ID, final String value)1434 Token (final int ID, final String value) 1435 { 1436 if (value == null) throw new IllegalArgumentException ("null input: value"); 1437 1438 m_ID = ID; 1439 m_value = value; 1440 } 1441 getID()1442 int getID () 1443 { 1444 return m_ID; 1445 } 1446 getValue()1447 String getValue () 1448 { 1449 return m_value; 1450 } 1451 toString()1452 public String toString () 1453 { 1454 return m_ID + ": [" + m_value + "]"; 1455 } 1456 1457 1458 static final int EOF_ID = 0; 1459 static final int STRING_ID = 1; 1460 static final int COLON_ID = 2; 1461 static final int SEMICOLON_ID = 3; 1462 static final int COMMA_ID = 4; 1463 static final int LBRACKET_ID = 5; 1464 static final int RBRACKET_ID = 6; 1465 static final int OPTIONAL_ID = 7; 1466 static final int REQUIRED_ID = 8; 1467 static final int CARD_ID = 9; 1468 static final int VALUES_ID = 10; 1469 static final int TEXT_ID = 11; 1470 static final int REQUIRES_ID = 12; 1471 static final int EXCLUDES_ID = 13; 1472 static final int MERGEABLE_ID = 14; 1473 static final int DETAILEDONLY_ID = 15; 1474 static final int PATTERN_ID = 16; 1475 1476 static final Token EOF = new Token (EOF_ID, "<EOF>"); 1477 static final Token COLON = new Token (COLON_ID, ":"); 1478 static final Token SEMICOLON = new Token (SEMICOLON_ID, ";"); 1479 static final Token COMMA = new Token (COMMA_ID, ","); 1480 static final Token LBRACKET = new Token (LBRACKET_ID, "{"); 1481 static final Token RBRACKET = new Token (RBRACKET_ID, "}"); 1482 static final Token OPTIONAL = new Token (OPTIONAL_ID, "optional"); 1483 static final Token REQUIRED = new Token (REQUIRED_ID, "required"); 1484 static final Token VALUES = new Token (VALUES_ID, "values"); 1485 static final Token REQUIRES = new Token (REQUIRES_ID, "requires"); 1486 static final Token EXCLUDES = new Token (EXCLUDES_ID, "excludes"); 1487 static final Token MERGEABLE = new Token (MERGEABLE_ID, "mergeable"); 1488 static final Token DETAILEDONLY = new Token (DETAILEDONLY_ID, "detailedonly"); 1489 static final Token PATTERN = new Token (PATTERN_ID, "pattern"); 1490 1491 private final int m_ID; 1492 private final String m_value; 1493 1494 } // end of nested class 1495 1496 parseOptDefMetadata(final Reader in, final String [] usageOpts)1497 private static OptDefMetadata parseOptDefMetadata (final Reader in, final String [] usageOpts) 1498 { 1499 final MetadataParser parser = new MetadataParser (); 1500 final OptDef [] optdefs = parser.parse (in); 1501 1502 // validate: 1503 1504 // for (int o = 0; o < optdefs.length; ++ o) 1505 // { 1506 // final OptDef optdef = optdefs [o]; 1507 // final int [] cardinality = optdef.getValueCardinality (); 1508 // 1509 // if (optdef.isMergeable ()) 1510 // { 1511 // if ((cardinality [1] != 0) && (cardinality [1] != Integer.MAX_VALUE)) 1512 // throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] is mergeable and can only specify {0, +inf} for max value cardinality: " + cardinality [1]); 1513 // } 1514 // } 1515 1516 final OptDefMetadata result = new OptDefMetadata (); 1517 for (int o = 0; o < optdefs.length; ++ o) 1518 { 1519 result.addOptDef (optdefs [o]); 1520 } 1521 1522 // add usage opts: 1523 if (usageOpts != null) 1524 { 1525 final OptDef usage = new OptDef (true); 1526 1527 usage.setNames (usageOpts); 1528 usage.setDescription ("display usage information"); 1529 usage.setValueCardinality (OptDef.C_ZERO); 1530 usage.setRequired (false); 1531 usage.setDetailedOnly (false); 1532 usage.setMergeable (false); 1533 1534 result.addOptDef (usage); 1535 } 1536 1537 // TODO: fix this to be pattern-savvy 1538 1539 for (int o = 0; o < optdefs.length; ++ o) 1540 { 1541 final OptDef optdef = optdefs [o]; 1542 1543 final String [] requires = optdef.getRequiresSet (); 1544 if (requires != null) 1545 { 1546 for (int r = 0; r < requires.length; ++ r) 1547 { 1548 final OptDef ropt = result.getOptDef (requires [r], null); 1549 if (ropt == null) 1550 throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies an unknown option [" + requires [r] + "] in its \'requires\' set"); 1551 1552 if (ropt == optdef) 1553 throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies itself in its \'requires\' set"); 1554 } 1555 } 1556 1557 final String [] excludes = optdef.getExcludesSet (); 1558 if (excludes != null) 1559 { 1560 for (int x = 0; x < excludes.length; ++ x) 1561 { 1562 final OptDef xopt = result.getOptDef (excludes [x], null); 1563 if (xopt == null) 1564 throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies an unknown option [" + excludes [x] + "] in its \'excludes\' set"); 1565 1566 if (xopt.isRequired ()) 1567 throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies a required option [" + excludes [x] + "] in its \'excludes\' set"); 1568 1569 if (xopt == optdef) 1570 throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies itself in its \'excludes\' set"); 1571 } 1572 } 1573 } 1574 1575 return result; 1576 } 1577 formatMessage(final String msg)1578 private String formatMessage (final String msg) 1579 { 1580 if (m_msgPrefix == null) return msg; 1581 else 1582 { 1583 return m_msgPrefix.concat (msg); 1584 } 1585 } 1586 1587 1588 private final String m_msgPrefix; 1589 private final OptDefMetadata m_metadata; 1590 1591 private static final int CANONICAL_OPT_PREFIX = 1; // indexes into OPT_PREFIXES 1592 private static final String [] OPT_PREFIXES = new String [] {"--", "-"}; // HACK: these must appear in decreasing length order 1593 private static final char [] OPT_VALUE_SEPARATORS = new char [] {':', '='}; 1594 1595 private static final int STATE_OPT = 0, STATE_OPT_VALUE = 1, STATE_FREE_ARGS = 2, STATE_ERROR = 3; 1596 1597 } // end of class 1598 // ----------------------------------------------------------------------------