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 */
9package com.vladium.util.args;
10
11import java.io.CharArrayWriter;
12import java.io.IOException;
13import java.io.InputStream;
14import java.io.InputStreamReader;
15import java.io.PrintWriter;
16import java.io.Reader;
17import java.util.ArrayList;
18import java.util.HashMap;
19import java.util.HashSet;
20import java.util.Iterator;
21import java.util.List;
22import java.util.Map;
23import java.util.Set;
24
25import com.vladium.util.IConstants;
26import com.vladium.util.ResourceLoader;
27
28// ----------------------------------------------------------------------------
29/**
30 * @author Vlad Roubtsov, (C) 2002
31 */
32final 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
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
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
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     */
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     */
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    {
508        public String getName ()
509        {
510            return m_name;
511        }
512
513        public String getCanonicalName ()
514        {
515            return m_canonicalName;
516        }
517
518        public int getValueCount ()
519        {
520            if (m_values == null) return 0;
521
522            return m_values.size ();
523        }
524
525        public String getFirstValue ()
526        {
527            if (m_values == null) return null;
528
529            return (String) m_values.get (0);
530        }
531
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
542        public String getPatternPrefix ()
543        {
544            return m_patternPrefix;
545        }
546
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
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
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    {
585        public int usageRequestLevel ()
586        {
587            return m_usageRequestLevel;
588        }
589
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
602        public String [] getFreeArgs ()
603        {
604            if (hasErrors ())
605                throw new IllegalStateException (errorsToString ());
606
607            return m_freeArgs;
608        }
609
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
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
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
651        Opts ()
652        {
653            m_opts = new ArrayList ();
654            m_nameMap = new HashMap ();
655            m_patternMap = new HashMap ();
656        }
657
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
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
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
721        void setUsageRequested (final int level)
722        {
723            m_usageRequestLevel = level;
724        }
725
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
736        boolean hasErrors ()
737        {
738            return (m_errors != null) && ! m_errors.isEmpty ();
739        }
740
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    {
770        OptDef (final boolean usage)
771        {
772            m_usage = usage;
773        }
774
775        boolean isUsage ()
776        {
777            return m_usage;
778        }
779
780        String getCanonicalName ()
781        {
782            return m_names [0];
783        }
784
785        String [] getNames ()
786        {
787            return m_names;
788        }
789
790        boolean isRequired ()
791        {
792            return m_required;
793        }
794
795        String getValueMnemonic ()
796        {
797            return m_valueMnemonic;
798        }
799
800        boolean isMergeable ()
801        {
802            return m_mergeable;
803        }
804
805        boolean isDetailedOnly ()
806        {
807            return m_detailedOnly;
808        }
809
810        boolean isPattern ()
811        {
812            return m_pattern;
813        }
814
815        int [] getValueCardinality ()
816        {
817            return m_valueCardinality;
818        }
819
820        String [] getRequiresSet ()
821        {
822            return m_requiresSet;
823        }
824
825        String [] getExcludesSet ()
826        {
827            return m_excludesSet;
828        }
829
830        String getDescription ()
831        {
832            return m_description;
833        }
834
835        void setNames (final String [] names)
836        {
837            if (names == null) throw new IllegalArgumentException ("null input: names");
838
839            m_names = names;
840        }
841
842        void setRequired (final boolean required)
843        {
844            m_required = required;
845        }
846
847        void setValueMnemonic (final String mnemonic)
848        {
849            if (mnemonic == null) throw new IllegalArgumentException ("null input: mnemonic");
850
851            m_valueMnemonic = mnemonic;
852        }
853
854        void setMergeable (final boolean mergeable)
855        {
856            m_mergeable = mergeable;
857        }
858
859        void setDetailedOnly (final boolean detailedOnly)
860        {
861            m_detailedOnly = detailedOnly;
862        }
863
864        void setPattern (final boolean pattern)
865        {
866            m_pattern = pattern;
867        }
868
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
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
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
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    {
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
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
959        Iterator /* OptDef */ getOptDefs ()
960        {
961            return m_optdefs.iterator ();
962        }
963
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
971        Set /* String(canonical name) */ getRequiredOpts ()
972        {
973            return m_requiredOpts;
974        }
975
976        OptDef getUsageOptDef ()
977        {
978            return m_usageOptDef;
979        }
980
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         */
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
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
1084         String [] optnamelist ()
1085         {
1086             return namelist ();
1087         }
1088
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
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
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
1206         Token accept ()
1207         {
1208             final Token current = m_token;
1209             nextToken ();
1210
1211             return current;
1212         }
1213
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
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
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
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
1387             KEYWORDS.put (Token.OPTIONAL.getValue (), Token.OPTIONAL);
1388             KEYWORDS.put (Token.REQUIRED.getValue (), Token.REQUIRED);
1389             KEYWORDS.put (Token.VALUES.getValue (), Token.VALUES);
1390             KEYWORDS.put (Token.REQUIRES.getValue (), Token.REQUIRES);
1391             KEYWORDS.put (Token.EXCLUDES.getValue (), Token.EXCLUDES);
1392             KEYWORDS.put (Token.MERGEABLE.getValue (), Token.MERGEABLE);
1393             KEYWORDS.put (Token.DETAILEDONLY.getValue (), Token.DETAILEDONLY);
1394             KEYWORDS.put (Token.PATTERN.getValue (), Token.PATTERN);
1395         }
1396
1397    } // end of nested class
1398
1399
1400    OptsParser (final String metadataResourceName, final ClassLoader loader, final String [] usageOpts)
1401    {
1402        this (metadataResourceName, loader, null, usageOpts);
1403    }
1404
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    {
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
1442        int getID ()
1443        {
1444            return m_ID;
1445        }
1446
1447        String getValue ()
1448        {
1449            return m_value;
1450        }
1451
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
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
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// ----------------------------------------------------------------------------