| /* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under |
| * the terms of the Common Public License v1.0 which accompanies this distribution, |
| * and is available at http://www.eclipse.org/legal/cpl-v10.html |
| * |
| * $Id: OptsParser.java,v 1.1.1.1 2004/05/09 16:57:57 vlad_r Exp $ |
| */ |
| package com.vladium.util.args; |
| |
| import java.io.CharArrayWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import com.vladium.util.IConstants; |
| import com.vladium.util.ResourceLoader; |
| |
| // ---------------------------------------------------------------------------- |
| /** |
| * @author Vlad Roubtsov, (C) 2002 |
| */ |
| final class OptsParser implements IOptsParser |
| { |
| // public: ................................................................ |
| |
| // TODO: #-comments |
| // TODO: prefixing for error messages |
| // TODO: support var subst (main class name, etc) |
| // TODO: support short/full usage |
| // TODO: support marking opts as for displayable in full usage only |
| |
| public synchronized void usage (final PrintWriter out, final int level, final int width) |
| { |
| // TODO: use width |
| // TODO: cache? |
| |
| final String prefix = OPT_PREFIXES [CANONICAL_OPT_PREFIX]; |
| |
| for (Iterator i = m_metadata.getOptDefs (); i.hasNext (); ) |
| { |
| final OptDef optdef = (OptDef) i.next (); |
| |
| if ((level < 2) && optdef.isDetailedOnly ()) // skip detailed usage only options |
| continue; |
| |
| final StringBuffer line = new StringBuffer (" "); |
| |
| final String canonicalName = optdef.getCanonicalName (); |
| final boolean isPattern = optdef.isPattern (); |
| |
| line.append (prefix); |
| line.append (canonicalName); |
| if (isPattern) line.append ('*'); |
| |
| final String [] names = optdef.getNames (); |
| for (int n = 0; n < names.length; ++ n) |
| { |
| final String name = names [n]; |
| if (! name.equals (canonicalName)) |
| { |
| line.append (", "); |
| |
| line.append (prefix); |
| line.append (name); |
| if (isPattern) line.append ('*'); |
| } |
| } |
| |
| final String vmnemonic = optdef.getValueMnemonic (); |
| if (vmnemonic != null) |
| { |
| line.append (' '); |
| line.append (vmnemonic); |
| } |
| |
| |
| int padding = 16 - line.length (); |
| if (padding < 2) |
| { |
| // end the current line |
| out.println (line); |
| |
| line.setLength (0); |
| for (int p = 0; p < 16; ++ p) line.append (' '); |
| } |
| else |
| { |
| for (int p = 0; p < padding; ++ p) line.append (' '); |
| } |
| |
| if (optdef.isRequired ()) line.append ("{required} "); |
| line.append (optdef.getDescription ()); |
| |
| out.println (line); |
| } |
| |
| if (level < DETAILED_USAGE) |
| { |
| final OptDef usageOptDef = m_metadata.getUsageOptDef (); |
| if ((usageOptDef != null) && (usageOptDef.getNames () != null) && (usageOptDef.getNames ().length > 1)) |
| { |
| out.println (); |
| out.println (" {use '" + usageOptDef.getNames () [1] + "' option to see detailed usage help}"); |
| } |
| } |
| } |
| |
| public synchronized IOpts parse (final String [] args) |
| { |
| if (args == null) throw new IllegalArgumentException ("null input: args"); |
| |
| final Opts opts = new Opts (); |
| |
| { |
| final String [] nv = new String [2]; // out buffer for getOptNameAndValue() |
| final String [] pp = new String [1]; // out buffer for getOptDef() |
| |
| // running state/current vars: |
| int state = STATE_OPT; |
| OptDef optdef = null; |
| Opt opt = null; |
| String value = null; |
| int valueCount = 0; |
| |
| int a; |
| scan: for (a = 0; a < args.length; ) |
| { |
| final String av = args [a]; |
| if (av == null) throw new IllegalArgumentException ("null input: args[" + a + "]"); |
| |
| //System.out.println ("[state: " + state + "] av = " + av); |
| |
| switch (state) |
| { |
| case STATE_OPT: |
| { |
| if (isOpt (av, valueCount, optdef)) |
| { |
| // 'av' looks like an option: get its name and see if it |
| // is in the metadata |
| |
| valueCount = 0; |
| |
| getOptNameAndValue (av, nv); // this can leave nv[1] as null |
| |
| // [assertion: nv [0] != null] |
| |
| final String optName = nv [0]; // is not necessarily canonical |
| optdef = m_metadata.getOptDef (optName, pp); // pp [0] is always set by this |
| |
| if (optdef == null) |
| { |
| // unknown option: |
| |
| // TODO: coded messages? |
| opts.addError (formatMessage ("unknown option \'" + optName + "\'")); |
| |
| state = STATE_ERROR; |
| } |
| else |
| { |
| // merge if necessary: |
| |
| final String canonicalName = getOptCanonicalName (optName, optdef); |
| final String patternPrefix = pp [0]; |
| |
| opt = opts.getOpt (canonicalName); |
| |
| if (optdef.isMergeable ()) |
| { |
| if (opt == null) |
| { |
| opt = new Opt (optName, canonicalName, patternPrefix); |
| opts.addOpt (opt, optdef, optName); |
| } |
| } |
| else |
| { |
| if (opt == null) |
| { |
| opt = new Opt (optName, canonicalName, patternPrefix); |
| opts.addOpt (opt, optdef, optName); |
| } |
| else |
| { |
| opts.addError (formatMessage ("option \'" + optName + "\' cannot be specified more than once")); |
| |
| state = STATE_ERROR; |
| } |
| } |
| |
| value = nv [1]; |
| |
| if (value == null) ++ a; |
| state = STATE_OPT_VALUE; |
| } |
| } |
| else |
| { |
| // not in STATE_OPT_VALUE and 'av' does not look |
| // like an option: the rest of args are free |
| |
| state = STATE_FREE_ARGS; |
| } |
| } |
| break; |
| |
| |
| case STATE_OPT_VALUE: |
| { |
| // [assertion: opt != null and optdef != null] |
| |
| if (value != null) |
| { |
| // value specified explicitly using the <name>separator<value> syntax: |
| // [don't shift a] |
| |
| valueCount = 1; |
| |
| final int [] cardinality = optdef.getValueCardinality (); |
| |
| if (cardinality [1] < 1) |
| { |
| opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept values: \'" + value + "\'")); |
| |
| state = STATE_ERROR; |
| } |
| else |
| { |
| ++ a; |
| opt.addValue (value); |
| } |
| } |
| else |
| { |
| value = args [a]; |
| |
| final int [] cardinality = optdef.getValueCardinality (); |
| |
| if (isOpt (value, valueCount, optdef)) |
| { |
| if (valueCount < cardinality [0]) |
| { |
| opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept fewer than " + cardinality [0] + " value(s)")); |
| |
| state = STATE_ERROR; |
| } |
| else |
| state = STATE_OPT; |
| } |
| else |
| { |
| if (valueCount < cardinality [1]) |
| { |
| ++ valueCount; |
| ++ a; |
| opt.addValue (value); |
| } |
| else |
| { |
| // this check is redundant: |
| // if (valueCount < cardinality [0]) |
| // { |
| // opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept fewer than " + cardinality [0] + " value(s)")); |
| // |
| // state = STATE_ERROR; |
| // } |
| // else |
| state = STATE_FREE_ARGS; |
| } |
| } |
| } |
| |
| value = null; |
| } |
| break; |
| |
| |
| case STATE_FREE_ARGS: |
| { |
| if (isOpt (args [a], valueCount, optdef)) |
| { |
| state = STATE_OPT; |
| } |
| else |
| { |
| opts.setFreeArgs (args, a); |
| break scan; |
| } |
| } |
| break; |
| |
| |
| case STATE_ERROR: |
| { |
| break scan; // TODO: could use the current value of 'a' for a better error message |
| } |
| |
| } // end of switch |
| } |
| |
| if (a == args.length) |
| { |
| if (opt != null) // validate the last option's min cardinality |
| { |
| final int [] cardinality = optdef.getValueCardinality (); |
| |
| if (valueCount < cardinality [0]) |
| { |
| opts.addError (formatMessage ("option \'" + opt.getName () + "\' does not accept fewer than " + cardinality [0] + " value(s)")); |
| } |
| } |
| else |
| { |
| opts.setFreeArgs (args, a); |
| } |
| } |
| |
| } // end of 'args' parsing |
| |
| |
| final IOpt [] specified = opts.getOpts (); |
| if (specified != null) |
| { |
| // validation: all required parameters must be specified |
| |
| final Set /* String(canonical name) */ required = new HashSet (); |
| required.addAll (m_metadata.getRequiredOpts ()); |
| |
| for (int s = 0; s < specified.length; ++ s) |
| { |
| required.remove (specified [s].getCanonicalName ()); |
| } |
| |
| if (! required.isEmpty ()) |
| { |
| for (Iterator i = required.iterator (); i.hasNext (); ) |
| { |
| opts.addError (formatMessage ("missing required option \'" + (String) i.next () + "\'")); |
| } |
| } |
| |
| for (int s = 0; s < specified.length; ++ s) |
| { |
| final IOpt opt = specified [s]; |
| final OptDef optdef = m_metadata.getOptDef (opt.getCanonicalName (), null); |
| |
| // // validation: value cardinality constraints |
| // |
| // final int [] cardinality = optdef.getValueCardinality (); |
| // if (opt.getValueCount () < cardinality [0]) |
| // opts.addError (formatMessage ("option \'" + opt.getName () + "\' must have at least " + cardinality [0] + " value(s)")); |
| // else if (opt.getValueCount () > cardinality [1]) |
| // opts.addError (formatMessage ("option \'" + opt.getName () + "\' must not have more than " + cardinality [1] + " value(s)")); |
| |
| // validation: "requires" constraints |
| |
| final String [] requires = optdef.getRequiresSet (); // not canonicalized |
| if (requires != null) |
| { |
| for (int r = 0; r < requires.length; ++ r) |
| { |
| if (opts.getOpt (requires [r]) == null) |
| opts.addError (formatMessage ("option \'" + opt.getName () + "\' requires option \'" + requires [r] + "\'")); |
| } |
| } |
| |
| // validation: "not with" constraints |
| |
| final String [] excludes = optdef.getExcludesSet (); // not canonicalized |
| if (excludes != null) |
| { |
| for (int x = 0; x < excludes.length; ++ x) |
| { |
| final Opt xopt = opts.getOpt (excludes [x]); |
| if (xopt != null) |
| opts.addError (formatMessage ("option \'" + opt.getName () + "\' cannot be used with option \'" + xopt.getName () + "\'")); |
| } |
| } |
| |
| // side effect: determine if usage is requested |
| |
| if (optdef.isUsage ()) |
| { |
| opts.setUsageRequested (opt.getName ().equals (opt.getCanonicalName ()) ? SHORT_USAGE : DETAILED_USAGE); |
| } |
| } |
| } |
| |
| return opts; |
| } |
| |
| private static String getOptCanonicalName (final String n, final OptDef optdef) |
| { |
| if (optdef.isPattern ()) |
| { |
| final String canonicalPattern = optdef.getCanonicalName (); |
| final String [] patterns = optdef.getNames (); |
| |
| for (int p = 0; p < patterns.length; ++ p) |
| { |
| final String pattern = patterns [p]; |
| |
| if (n.startsWith (pattern)) |
| { |
| return canonicalPattern.concat (n.substring (pattern.length ())); |
| } |
| } |
| |
| // this should never happen: |
| throw new IllegalStateException ("failed to detect pattern prefix for [" + n + "]"); |
| } |
| else |
| { |
| return optdef.getCanonicalName (); |
| } |
| } |
| |
| /* |
| * ['optdef' can be null if no current opt def context has been established] |
| * |
| * pre: av != null |
| * input not validated |
| */ |
| private static boolean isOpt (final String av, final int valueCount, final OptDef optdef) |
| { |
| if (optdef != null) |
| { |
| // if the current optdef calls for more values, consume the next token |
| // as an op value greedily, without looking at its prefix: |
| |
| final int [] cardinality = optdef.getValueCardinality (); |
| |
| if (valueCount < cardinality [1]) return false; |
| } |
| |
| // else check av's prefix: |
| |
| for (int p = 0; p < OPT_PREFIXES.length; ++ p) |
| { |
| if (av.startsWith (OPT_PREFIXES [p])) |
| return (av.length () > OPT_PREFIXES [p].length ()); |
| } |
| |
| return false; |
| } |
| |
| /* |
| * pre: av != null and isOpt(av)=true |
| * input not validated |
| */ |
| private static void getOptNameAndValue (final String av, final String [] nv) |
| { |
| nv [0] = null; |
| nv [1] = null; |
| |
| for (int p = 0; p < OPT_PREFIXES.length; ++ p) |
| { |
| if ((av.startsWith (OPT_PREFIXES [p])) && (av.length () > OPT_PREFIXES [p].length ())) |
| { |
| final String name = av.substring (OPT_PREFIXES [p].length ()); // with a possible value after a separator |
| |
| char separator = 0; |
| int sindex = Integer.MAX_VALUE; |
| |
| for (int s = 0; s < OPT_VALUE_SEPARATORS.length; ++ s) |
| { |
| final int index = name.indexOf (OPT_VALUE_SEPARATORS [s]); |
| if ((index > 0) && (index < sindex)) |
| { |
| separator = OPT_VALUE_SEPARATORS [s]; |
| sindex = index; |
| } |
| } |
| |
| if (separator != 0) |
| { |
| nv [0] = name.substring (0, sindex); |
| nv [1] = name.substring (sindex + 1); |
| } |
| else |
| { |
| nv [0] = name; |
| } |
| |
| return; |
| } |
| } |
| } |
| |
| // protected: ............................................................. |
| |
| // package: ............................................................... |
| |
| |
| static final class Opt implements IOptsParser.IOpt |
| { |
| public String getName () |
| { |
| return m_name; |
| } |
| |
| public String getCanonicalName () |
| { |
| return m_canonicalName; |
| } |
| |
| public int getValueCount () |
| { |
| if (m_values == null) return 0; |
| |
| return m_values.size (); |
| } |
| |
| public String getFirstValue () |
| { |
| if (m_values == null) return null; |
| |
| return (String) m_values.get (0); |
| } |
| |
| public String [] getValues () |
| { |
| if (m_values == null) return IConstants.EMPTY_STRING_ARRAY; |
| |
| final String [] result = new String [m_values.size ()]; |
| m_values.toArray (result); |
| |
| return result; |
| } |
| |
| public String getPatternPrefix () |
| { |
| return m_patternPrefix; |
| } |
| |
| public String toString () |
| { |
| final StringBuffer s = new StringBuffer (m_name); |
| if (! m_canonicalName.equals (m_name)) s.append (" [" + m_canonicalName + "]"); |
| |
| if (m_values != null) |
| { |
| s.append (": "); |
| s.append (m_values); |
| } |
| |
| return s.toString (); |
| } |
| |
| Opt (final String name, final String canonicalName, final String patternPrefix) |
| { |
| m_name = name; |
| m_canonicalName = canonicalName; |
| m_patternPrefix = patternPrefix; |
| } |
| |
| void addValue (final String value) |
| { |
| if (value == null) throw new IllegalArgumentException ("null input: value"); |
| |
| if (m_values == null) m_values = new ArrayList (); |
| m_values.add (value); |
| } |
| |
| |
| private final String m_name, m_canonicalName, m_patternPrefix; |
| private ArrayList m_values; |
| |
| } // end of nested class |
| |
| |
| static final class Opts implements IOptsParser.IOpts |
| { |
| public int usageRequestLevel () |
| { |
| return m_usageRequestLevel; |
| } |
| |
| public void error (final PrintWriter out, final int width) |
| { |
| // TODO: use width |
| if (hasErrors ()) |
| { |
| for (Iterator i = m_errors.iterator (); i.hasNext (); ) |
| { |
| out.println (i.next ()); |
| } |
| } |
| } |
| |
| public String [] getFreeArgs () |
| { |
| if (hasErrors ()) |
| throw new IllegalStateException (errorsToString ()); |
| |
| return m_freeArgs; |
| } |
| |
| public IOpt [] getOpts () |
| { |
| if (hasErrors ()) return null; |
| |
| if (m_opts.isEmpty ()) |
| return EMPTY_OPT_ARRAY; |
| else |
| { |
| final IOpt [] result = new IOpt [m_opts.size ()]; |
| m_opts.toArray (result); |
| |
| return result; |
| } |
| } |
| |
| public IOpt [] getOpts (final String pattern) |
| { |
| if (hasErrors ()) return null; |
| |
| final List /* Opt */ patternOpts = (List) m_patternMap.get (pattern); |
| |
| if ((patternOpts == null) || patternOpts.isEmpty ()) |
| return EMPTY_OPT_ARRAY; |
| else |
| { |
| final IOpt [] result = new IOpt [patternOpts.size ()]; |
| patternOpts.toArray (result); |
| |
| return result; |
| } |
| } |
| |
| |
| public boolean hasArg (final String name) |
| { |
| if (hasErrors ()) |
| throw new IllegalStateException (errorsToString ()); |
| |
| return m_nameMap.containsKey (name); |
| } |
| |
| Opts () |
| { |
| m_opts = new ArrayList (); |
| m_nameMap = new HashMap (); |
| m_patternMap = new HashMap (); |
| } |
| |
| void addOpt (final Opt opt, final OptDef optdef, final String occuranceName) |
| { |
| if (opt == null) throw new IllegalArgumentException ("null input: opt"); |
| if (optdef == null) throw new IllegalArgumentException ("null input: optdef"); |
| if (occuranceName == null) throw new IllegalArgumentException ("null input: occuranceName"); |
| |
| // [name collisions detected elsewhere] |
| |
| m_opts.add (opt); |
| |
| final String [] names = optdef.getNames (); |
| final boolean isPattern = (opt.getPatternPrefix () != null); |
| |
| if (isPattern) |
| { |
| final String unprefixedName = occuranceName.substring (opt.getPatternPrefix ().length ()); |
| |
| for (int n = 0; n < names.length; ++ n) |
| { |
| m_nameMap.put (names [n].concat (unprefixedName), opt); |
| } |
| |
| { |
| final String canonicalPattern = optdef.getCanonicalName (); |
| |
| List patternList = (List) m_patternMap.get (canonicalPattern); |
| if (patternList == null) |
| { |
| patternList = new ArrayList (); |
| for (int n = 0; n < names.length; ++ n) |
| { |
| m_patternMap.put (names [n], patternList); |
| } |
| } |
| |
| patternList.add (opt); |
| } |
| } |
| else |
| { |
| for (int n = 0; n < names.length; ++ n) |
| { |
| m_nameMap.put (names [n], opt); |
| } |
| } |
| } |
| |
| Opt getOpt (final String occuranceName) |
| { |
| if (occuranceName == null) throw new IllegalArgumentException ("null input: occuranceName"); |
| |
| return (Opt) m_nameMap.get (occuranceName); |
| } |
| |
| void setFreeArgs (final String [] args, final int start) |
| { |
| if (args == null) throw new IllegalArgumentException ("null input: args"); |
| if ((start < 0) || (start > args.length)) throw new IllegalArgumentException ("invalid start index: " + start); |
| |
| m_freeArgs = new String [args.length - start]; |
| System.arraycopy (args, start, m_freeArgs, 0, m_freeArgs.length); |
| } |
| |
| void setUsageRequested (final int level) |
| { |
| m_usageRequestLevel = level; |
| } |
| |
| void addError (final String msg) |
| { |
| if (msg != null) |
| { |
| if (m_errors == null) m_errors = new ArrayList (); |
| |
| m_errors.add (msg); |
| } |
| } |
| |
| boolean hasErrors () |
| { |
| return (m_errors != null) && ! m_errors.isEmpty (); |
| } |
| |
| String errorsToString () |
| { |
| if (! hasErrors ()) return "<no errors>"; |
| |
| final CharArrayWriter caw = new CharArrayWriter (); |
| final PrintWriter pw = new PrintWriter (caw); |
| |
| error (pw, DEFAULT_ERROR_WIDTH); |
| pw.flush (); |
| |
| return caw.toString (); |
| } |
| |
| |
| private final List /* Opt */ m_opts; |
| private final Map /* String(name/pattern-prefixed name)->Opt */ m_nameMap; |
| private final Map /* String(pattern prefix)->List<Opt> */ m_patternMap; |
| private String [] m_freeArgs; |
| private List /* String */ m_errors; |
| private int m_usageRequestLevel; |
| |
| private static final int DEFAULT_ERROR_WIDTH = 80; |
| private static final IOpt [] EMPTY_OPT_ARRAY = new IOpt [0]; |
| |
| } // end of nested class |
| |
| |
| static final class OptDef // TODO: merge with Opt? |
| { |
| OptDef (final boolean usage) |
| { |
| m_usage = usage; |
| } |
| |
| boolean isUsage () |
| { |
| return m_usage; |
| } |
| |
| String getCanonicalName () |
| { |
| return m_names [0]; |
| } |
| |
| String [] getNames () |
| { |
| return m_names; |
| } |
| |
| boolean isRequired () |
| { |
| return m_required; |
| } |
| |
| String getValueMnemonic () |
| { |
| return m_valueMnemonic; |
| } |
| |
| boolean isMergeable () |
| { |
| return m_mergeable; |
| } |
| |
| boolean isDetailedOnly () |
| { |
| return m_detailedOnly; |
| } |
| |
| boolean isPattern () |
| { |
| return m_pattern; |
| } |
| |
| int [] getValueCardinality () |
| { |
| return m_valueCardinality; |
| } |
| |
| String [] getRequiresSet () |
| { |
| return m_requiresSet; |
| } |
| |
| String [] getExcludesSet () |
| { |
| return m_excludesSet; |
| } |
| |
| String getDescription () |
| { |
| return m_description; |
| } |
| |
| void setNames (final String [] names) |
| { |
| if (names == null) throw new IllegalArgumentException ("null input: names"); |
| |
| m_names = names; |
| } |
| |
| void setRequired (final boolean required) |
| { |
| m_required = required; |
| } |
| |
| void setValueMnemonic (final String mnemonic) |
| { |
| if (mnemonic == null) throw new IllegalArgumentException ("null input: mnemonic"); |
| |
| m_valueMnemonic = mnemonic; |
| } |
| |
| void setMergeable (final boolean mergeable) |
| { |
| m_mergeable = mergeable; |
| } |
| |
| void setDetailedOnly (final boolean detailedOnly) |
| { |
| m_detailedOnly = detailedOnly; |
| } |
| |
| void setPattern (final boolean pattern) |
| { |
| m_pattern = pattern; |
| } |
| |
| void setValueCardinality (final int [] cardinality) |
| { |
| if ((cardinality == null) || (cardinality.length != 2)) throw new IllegalArgumentException ("null or invalid input: cardinality"); |
| |
| m_valueCardinality = cardinality; |
| } |
| |
| void setRequiresSet (final String [] names) |
| { |
| if (names == null) throw new IllegalArgumentException ("null input: names"); |
| |
| m_requiresSet = names.length > 0 ? names : null; |
| } |
| |
| void setExcludesSet (final String [] names) |
| { |
| if (names == null) throw new IllegalArgumentException ("null input: names"); |
| |
| m_excludesSet = names.length > 0 ? names : null; |
| } |
| |
| void setDescription (final String description) |
| { |
| if (description == null) throw new IllegalArgumentException ("null input: description"); |
| |
| m_description = description; |
| } |
| |
| |
| static final int [] C_ZERO = new int [] {0, 0}; |
| static final int [] C_ONE = new int [] {1, 1}; |
| static final int [] C_ZERO_OR_ONE = new int [] {0, 1}; |
| static final int [] C_ZERO_OR_MORE = new int [] {0, Integer.MAX_VALUE}; |
| static final int [] C_ONE_OR_MORE = new int [] {1, Integer.MAX_VALUE}; |
| |
| |
| private final boolean m_usage; |
| private String [] m_names; |
| private boolean m_required; |
| private String m_valueMnemonic; |
| private boolean m_mergeable; |
| private boolean m_detailedOnly; |
| private boolean m_pattern; |
| private int [] m_valueCardinality; |
| private String [] m_requiresSet, m_excludesSet; |
| private String m_description; |
| |
| } // end of nested class |
| |
| |
| static final class OptDefMetadata |
| { |
| OptDefMetadata () |
| { |
| m_optdefs = new ArrayList (); |
| m_optdefMap = new HashMap (); |
| m_requiredOpts = new HashSet (); |
| m_patternOptDefMap = new HashMap (); |
| } |
| |
| OptDef getOptDef (final String name, final String [] prefixout) |
| { |
| if (name == null) throw new IllegalArgumentException ("null input: name"); |
| |
| if (prefixout != null) prefixout [0] = null; |
| |
| // first, see if this is a regular option: |
| OptDef result = (OptDef) m_optdefMap.get (name); |
| |
| // next, see if this is a prefixed option: |
| if (result == null) |
| { |
| for (Iterator ps = m_patternOptDefMap.entrySet ().iterator (); |
| ps.hasNext (); ) |
| { |
| final Map.Entry entry = (Map.Entry) ps.next (); |
| final String pattern = (String) entry.getKey (); |
| |
| if (name.startsWith (pattern)) |
| { |
| if (prefixout != null) prefixout [0] = pattern; |
| result = (OptDef) entry.getValue (); |
| break; |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| Iterator /* OptDef */ getOptDefs () |
| { |
| return m_optdefs.iterator (); |
| } |
| |
| OptDef getPatternOptDefs (final String pattern) // returns null if no such pattern is defined |
| { |
| if (pattern == null) throw new IllegalArgumentException ("null input: pattern"); |
| |
| return (OptDef) m_patternOptDefMap.get (pattern); |
| } |
| |
| Set /* String(canonical name) */ getRequiredOpts () |
| { |
| return m_requiredOpts; |
| } |
| |
| OptDef getUsageOptDef () |
| { |
| return m_usageOptDef; |
| } |
| |
| void addOptDef (final OptDef optdef) |
| { |
| if (optdef == null) throw new IllegalArgumentException ("null input: optdef"); |
| |
| final Map map = optdef.isPattern () ? m_patternOptDefMap : m_optdefMap; |
| final String [] names = optdef.getNames (); |
| |
| for (int n = 0; n < names.length; ++ n) |
| { |
| if (map.containsKey (names [n])) |
| throw new IllegalArgumentException ("duplicate option name [" + names [n] + "]"); |
| |
| map.put (names [n], optdef); |
| } |
| |
| m_optdefs.add (optdef); |
| |
| if (optdef.isRequired ()) |
| m_requiredOpts.add (optdef.getCanonicalName ()); |
| |
| if (optdef.isUsage ()) |
| { |
| if (m_usageOptDef != null) |
| throw new IllegalArgumentException ("usage optdef set already"); |
| |
| m_usageOptDef = optdef; |
| } |
| } |
| |
| |
| final List /* OptDef */ m_optdefs; // keeps the addition order |
| final Map /* String(name)->OptDef */ m_optdefMap; |
| final Set /* String(canonical name) */ m_requiredOpts; |
| final Map /* String(pattern name)->OptDef */ m_patternOptDefMap; |
| private OptDef m_usageOptDef; |
| |
| } // end of nested class |
| |
| |
| static final class MetadataParser |
| { |
| /* |
| * metadata := ( optdef )* <EOF> |
| * |
| * optdef := optnamelist ":" optmetadata ";" |
| * optnamelist := namelist |
| * optmetadata := |
| * ("optional" | "required" ) |
| * [ "," "mergeable" ] |
| * [ "," "detailedonly" ] |
| * [ "," "pattern" ] |
| * "," "values" ":" cardinality |
| * [ "," name ] |
| * [ "," "requires" "{" namelist "}" ] |
| * [ "," "notwith" "{" namelist "}" ] |
| * "," text |
| * cardinality := "0" | "1" | "?" |
| * namelist := name ( "," name )* |
| * name := <single quoted string> |
| * text := <double quoted string> |
| */ |
| OptDef [] parse (final Reader in) |
| { |
| if (in == null) throw new IllegalArgumentException ("null input: in"); |
| m_in = in; |
| |
| nextChar (); |
| nextToken (); |
| |
| while (m_token != Token.EOF) |
| { |
| if (m_opts == null) m_opts = new ArrayList (); |
| m_opts.add (optdef ()); |
| } |
| |
| final OptDef [] result; |
| |
| if ((m_opts == null) || (m_opts.size () == 0)) |
| result = EMPTY_OPTDEF_ARRAY; |
| else |
| { |
| result = new OptDef [m_opts.size ()]; |
| m_opts.toArray (result); |
| } |
| |
| m_in = null; |
| m_opts = null; |
| |
| return result; |
| } |
| |
| OptDef optdef () |
| { |
| final OptDef optdef = new OptDef (false); |
| |
| optdef.setNames (optnamelist ()); |
| accept (Token.COLON_ID); |
| optmetadata (optdef); |
| accept (Token.SEMICOLON_ID); |
| |
| return optdef; |
| } |
| |
| String [] optnamelist () |
| { |
| return namelist (); |
| } |
| |
| void optmetadata (final OptDef optdef) |
| { |
| switch (m_token.getID ()) |
| { |
| case Token.REQUIRED_ID: |
| { |
| accept (); |
| optdef.setRequired (true); |
| } |
| break; |
| |
| case Token.OPTIONAL_ID: |
| { |
| accept (); |
| optdef.setRequired (false); |
| } |
| break; |
| |
| default: |
| throw new IllegalArgumentException ("parse error: invalid token " + m_token + ", expected " + Token.REQUIRED + " or " + Token.OPTIONAL); |
| |
| } // end of switch |
| |
| accept (Token.COMMA_ID); |
| |
| if (m_token.getID () == Token.MERGEABLE_ID) |
| { |
| accept (); |
| optdef.setMergeable (true); |
| |
| accept (Token.COMMA_ID); |
| } |
| |
| if (m_token.getID () == Token.DETAILEDONLY_ID) |
| { |
| accept (); |
| optdef.setDetailedOnly (true); |
| |
| accept (Token.COMMA_ID); |
| } |
| |
| if (m_token.getID () == Token.PATTERN_ID) |
| { |
| accept (); |
| optdef.setPattern (true); |
| |
| accept (Token.COMMA_ID); |
| } |
| |
| accept (Token.VALUES_ID); |
| accept (Token.COLON_ID); |
| optdef.setValueCardinality (cardinality ()); |
| |
| accept (Token.COMMA_ID); |
| if (m_token.getID () == Token.STRING_ID) |
| { |
| optdef.setValueMnemonic (m_token.getValue ()); |
| accept (); |
| |
| accept (Token.COMMA_ID); |
| } |
| |
| if (m_token.getID () == Token.REQUIRES_ID) |
| { |
| accept (); |
| |
| accept (Token.LBRACKET_ID); |
| optdef.setRequiresSet (namelist ()); |
| accept (Token.RBRACKET_ID); |
| |
| accept (Token.COMMA_ID); |
| } |
| |
| if (m_token.getID () == Token.EXCLUDES_ID) |
| { |
| accept (); |
| |
| accept (Token.LBRACKET_ID); |
| optdef.setExcludesSet (namelist ()); |
| accept (Token.RBRACKET_ID); |
| |
| accept (Token.COMMA_ID); |
| } |
| |
| optdef.setDescription (accept (Token.TEXT_ID).getValue ()); |
| } |
| |
| int [] cardinality () |
| { |
| final Token result = accept (Token.CARD_ID); |
| |
| if ("0".equals (result.getValue ())) |
| return OptDef.C_ZERO; |
| else if ("1".equals (result.getValue ())) |
| return OptDef.C_ONE; |
| else // ? |
| return OptDef.C_ZERO_OR_ONE; |
| } |
| |
| String [] namelist () |
| { |
| final List _result = new ArrayList (); |
| |
| _result.add (accept (Token.STRING_ID).getValue ()); |
| while (m_token.getID () == Token.COMMA_ID) |
| { |
| accept (); |
| _result.add (accept (Token.STRING_ID).getValue ()); |
| } |
| |
| final String [] result = new String [_result.size ()]; |
| _result.toArray (result); |
| |
| return result; |
| } |
| |
| |
| Token accept () |
| { |
| final Token current = m_token; |
| nextToken (); |
| |
| return current; |
| } |
| |
| Token accept (final int tokenID) |
| { |
| final Token current = m_token; |
| |
| if (m_token.getID () == tokenID) |
| nextToken (); |
| else |
| throw new IllegalArgumentException ("parse error: invalid token [" + m_token + "], expected type [" + tokenID + "]"); |
| |
| return current; |
| } |
| |
| // "scanner": |
| |
| void nextToken () |
| { |
| consumeWS (); |
| |
| switch (m_currentChar) |
| { |
| case -1: m_token = Token.EOF; break; |
| |
| case ':': |
| { |
| nextChar (); |
| m_token = Token.COLON; |
| } |
| break; |
| |
| case ';': |
| { |
| nextChar (); |
| m_token = Token.SEMICOLON; |
| } |
| break; |
| |
| case ',': |
| { |
| nextChar (); |
| m_token = Token.COMMA; |
| } |
| break; |
| |
| case '{': |
| { |
| nextChar (); |
| m_token = Token.LBRACKET; |
| } |
| break; |
| |
| case '}': |
| { |
| nextChar (); |
| m_token = Token.RBRACKET; |
| } |
| break; |
| |
| case '0': |
| { |
| nextChar (); |
| m_token = new Token (Token.CARD_ID, "0"); |
| } |
| break; |
| |
| case '1': |
| { |
| nextChar (); |
| m_token = new Token (Token.CARD_ID, "1"); |
| } |
| break; |
| |
| case '?': |
| { |
| nextChar (); |
| m_token = new Token (Token.CARD_ID, "?"); |
| } |
| break; |
| |
| case '\'': |
| { |
| final StringBuffer value = new StringBuffer (); |
| |
| nextChar (); |
| while (m_currentChar != '\'') |
| { |
| value.append ((char) m_currentChar); |
| nextChar (); |
| } |
| nextChar (); |
| |
| m_token = new Token (Token.STRING_ID, value.toString ()); |
| } |
| break; |
| |
| case '\"': |
| { |
| final StringBuffer value = new StringBuffer (); |
| |
| nextChar (); |
| while (m_currentChar != '\"') |
| { |
| value.append ((char) m_currentChar); |
| nextChar (); |
| } |
| nextChar (); |
| |
| m_token = new Token (Token.TEXT_ID, value.toString ()); |
| } |
| break; |
| |
| default: |
| { |
| final StringBuffer value = new StringBuffer (); |
| |
| while (Character.isLetter ((char) m_currentChar)) |
| { |
| value.append ((char) m_currentChar); |
| nextChar (); |
| } |
| |
| final Token token = (Token) KEYWORDS.get (value.toString ()); |
| if (token == null) |
| throw new IllegalArgumentException ("parse error: unrecognized keyword [" + value + "]"); |
| |
| m_token = token; |
| } |
| |
| } // end of switch |
| } |
| |
| |
| private void consumeWS () |
| { |
| if (m_currentChar == -1) |
| return; |
| else |
| { |
| while (Character.isWhitespace ((char) m_currentChar)) |
| { |
| nextChar (); |
| } |
| } |
| |
| // TODO: #-comments |
| } |
| |
| private void nextChar () |
| { |
| try |
| { |
| m_currentChar = m_in.read (); |
| } |
| catch (IOException ioe) |
| { |
| throw new RuntimeException ("I/O error while parsing: " + ioe); |
| } |
| } |
| |
| |
| private Reader m_in; |
| private List m_opts; |
| |
| private Token m_token; |
| private int m_currentChar; |
| |
| private static final Map KEYWORDS; |
| |
| private static final OptDef [] EMPTY_OPTDEF_ARRAY = new OptDef [0]; |
| |
| static |
| { |
| KEYWORDS = new HashMap (17); |
| |
| KEYWORDS.put (Token.OPTIONAL.getValue (), Token.OPTIONAL); |
| KEYWORDS.put (Token.REQUIRED.getValue (), Token.REQUIRED); |
| KEYWORDS.put (Token.VALUES.getValue (), Token.VALUES); |
| KEYWORDS.put (Token.REQUIRES.getValue (), Token.REQUIRES); |
| KEYWORDS.put (Token.EXCLUDES.getValue (), Token.EXCLUDES); |
| KEYWORDS.put (Token.MERGEABLE.getValue (), Token.MERGEABLE); |
| KEYWORDS.put (Token.DETAILEDONLY.getValue (), Token.DETAILEDONLY); |
| KEYWORDS.put (Token.PATTERN.getValue (), Token.PATTERN); |
| } |
| |
| } // end of nested class |
| |
| |
| OptsParser (final String metadataResourceName, final ClassLoader loader, final String [] usageOpts) |
| { |
| this (metadataResourceName, loader, null, usageOpts); |
| } |
| |
| OptsParser (final String metadataResourceName, final ClassLoader loader, final String msgPrefix, final String [] usageOpts) |
| { |
| if (metadataResourceName == null) throw new IllegalArgumentException ("null input: metadataResourceName"); |
| |
| m_msgPrefix = msgPrefix; |
| |
| InputStream in = null; |
| try |
| { |
| in = ResourceLoader.getResourceAsStream (metadataResourceName, loader); |
| if (in == null) |
| throw new IllegalArgumentException ("resource [" + metadataResourceName + "] could not be loaded via [" + loader + "]"); |
| |
| // TODO: encoding |
| final Reader rin = new InputStreamReader (in); |
| |
| m_metadata = parseOptDefMetadata (rin, usageOpts); |
| } |
| finally |
| { |
| if (in != null) try { in.close (); } catch (IOException ignore) {} |
| } |
| } |
| |
| // private: ............................................................... |
| |
| |
| private static final class Token |
| { |
| Token (final int ID, final String value) |
| { |
| if (value == null) throw new IllegalArgumentException ("null input: value"); |
| |
| m_ID = ID; |
| m_value = value; |
| } |
| |
| int getID () |
| { |
| return m_ID; |
| } |
| |
| String getValue () |
| { |
| return m_value; |
| } |
| |
| public String toString () |
| { |
| return m_ID + ": [" + m_value + "]"; |
| } |
| |
| |
| static final int EOF_ID = 0; |
| static final int STRING_ID = 1; |
| static final int COLON_ID = 2; |
| static final int SEMICOLON_ID = 3; |
| static final int COMMA_ID = 4; |
| static final int LBRACKET_ID = 5; |
| static final int RBRACKET_ID = 6; |
| static final int OPTIONAL_ID = 7; |
| static final int REQUIRED_ID = 8; |
| static final int CARD_ID = 9; |
| static final int VALUES_ID = 10; |
| static final int TEXT_ID = 11; |
| static final int REQUIRES_ID = 12; |
| static final int EXCLUDES_ID = 13; |
| static final int MERGEABLE_ID = 14; |
| static final int DETAILEDONLY_ID = 15; |
| static final int PATTERN_ID = 16; |
| |
| static final Token EOF = new Token (EOF_ID, "<EOF>"); |
| static final Token COLON = new Token (COLON_ID, ":"); |
| static final Token SEMICOLON = new Token (SEMICOLON_ID, ";"); |
| static final Token COMMA = new Token (COMMA_ID, ","); |
| static final Token LBRACKET = new Token (LBRACKET_ID, "{"); |
| static final Token RBRACKET = new Token (RBRACKET_ID, "}"); |
| static final Token OPTIONAL = new Token (OPTIONAL_ID, "optional"); |
| static final Token REQUIRED = new Token (REQUIRED_ID, "required"); |
| static final Token VALUES = new Token (VALUES_ID, "values"); |
| static final Token REQUIRES = new Token (REQUIRES_ID, "requires"); |
| static final Token EXCLUDES = new Token (EXCLUDES_ID, "excludes"); |
| static final Token MERGEABLE = new Token (MERGEABLE_ID, "mergeable"); |
| static final Token DETAILEDONLY = new Token (DETAILEDONLY_ID, "detailedonly"); |
| static final Token PATTERN = new Token (PATTERN_ID, "pattern"); |
| |
| private final int m_ID; |
| private final String m_value; |
| |
| } // end of nested class |
| |
| |
| private static OptDefMetadata parseOptDefMetadata (final Reader in, final String [] usageOpts) |
| { |
| final MetadataParser parser = new MetadataParser (); |
| final OptDef [] optdefs = parser.parse (in); |
| |
| // validate: |
| |
| // for (int o = 0; o < optdefs.length; ++ o) |
| // { |
| // final OptDef optdef = optdefs [o]; |
| // final int [] cardinality = optdef.getValueCardinality (); |
| // |
| // if (optdef.isMergeable ()) |
| // { |
| // if ((cardinality [1] != 0) && (cardinality [1] != Integer.MAX_VALUE)) |
| // throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] is mergeable and can only specify {0, +inf} for max value cardinality: " + cardinality [1]); |
| // } |
| // } |
| |
| final OptDefMetadata result = new OptDefMetadata (); |
| for (int o = 0; o < optdefs.length; ++ o) |
| { |
| result.addOptDef (optdefs [o]); |
| } |
| |
| // add usage opts: |
| if (usageOpts != null) |
| { |
| final OptDef usage = new OptDef (true); |
| |
| usage.setNames (usageOpts); |
| usage.setDescription ("display usage information"); |
| usage.setValueCardinality (OptDef.C_ZERO); |
| usage.setRequired (false); |
| usage.setDetailedOnly (false); |
| usage.setMergeable (false); |
| |
| result.addOptDef (usage); |
| } |
| |
| // TODO: fix this to be pattern-savvy |
| |
| for (int o = 0; o < optdefs.length; ++ o) |
| { |
| final OptDef optdef = optdefs [o]; |
| |
| final String [] requires = optdef.getRequiresSet (); |
| if (requires != null) |
| { |
| for (int r = 0; r < requires.length; ++ r) |
| { |
| final OptDef ropt = result.getOptDef (requires [r], null); |
| if (ropt == null) |
| throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies an unknown option [" + requires [r] + "] in its \'requires\' set"); |
| |
| if (ropt == optdef) |
| throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies itself in its \'requires\' set"); |
| } |
| } |
| |
| final String [] excludes = optdef.getExcludesSet (); |
| if (excludes != null) |
| { |
| for (int x = 0; x < excludes.length; ++ x) |
| { |
| final OptDef xopt = result.getOptDef (excludes [x], null); |
| if (xopt == null) |
| throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies an unknown option [" + excludes [x] + "] in its \'excludes\' set"); |
| |
| if (xopt.isRequired ()) |
| throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies a required option [" + excludes [x] + "] in its \'excludes\' set"); |
| |
| if (xopt == optdef) |
| throw new IllegalArgumentException ("option [" + optdef.getCanonicalName () + "] specifies itself in its \'excludes\' set"); |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| private String formatMessage (final String msg) |
| { |
| if (m_msgPrefix == null) return msg; |
| else |
| { |
| return m_msgPrefix.concat (msg); |
| } |
| } |
| |
| |
| private final String m_msgPrefix; |
| private final OptDefMetadata m_metadata; |
| |
| private static final int CANONICAL_OPT_PREFIX = 1; // indexes into OPT_PREFIXES |
| private static final String [] OPT_PREFIXES = new String [] {"--", "-"}; // HACK: these must appear in decreasing length order |
| private static final char [] OPT_VALUE_SEPARATORS = new char [] {':', '='}; |
| |
| private static final int STATE_OPT = 0, STATE_OPT_VALUE = 1, STATE_FREE_ARGS = 2, STATE_ERROR = 3; |
| |
| } // end of class |
| // ---------------------------------------------------------------------------- |