package dsl99lux ;

import dsl99lux.TextListParser ;
import dsl99lux.luxException ;
import dsl99lux.cmdException ;
import dsl99lux.cmdUnknownMethod ;
import java.lang.reflect.* ;
import java.util.Hashtable ;
import java.util.Enumeration ;

/**
 * NOTE: The match algorithm used by CmdLineParser.apply
 * is an early version of the algorithm reported for
 * extensionParser.apply in the paper. This version matches
 * Java Strings to method parameters rather than abstract
 * extensionObjects-to-parameters, but the match algorithm and the use
 * of Java reflection remains the same.

 * Copyright (c) 1998-1999  Lucent Technologies, Inc.
 *
 * This software is copyrighted by Lucent Technologies Inc.
 * The following terms apply to all files associated with the
 * software unless explicitly disclaimed in individual files.
 * 
 * The authors hereby grant permission to use and copy this
 * software and its documentation for educational, research
 * and reference purposes provided that existing copyright
 * notices are retained in all copies and that this notice is
 * included verbatim in any distributions. No written agreement,
 * license, or royalty fee is required for such authorized uses.
 * Modifications to this software may not be made without permission.
 * 
 * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY
 * PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL
 * DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION,
 * OR ANY DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
 * IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
 * NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS,
 * OR MODIFICATIONS.
 * 
 * GOVERNMENT USE:  If you are acquiring this software on behalf of the
 * U.S. Government, the Government shall have only "Restricted Rights"
 * in the software and related documentation as defined in the Federal
 * Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
 * are acquiring the software on behalf of the Department of Defense,
 * the software shall be classified as "Commercial Computer Software"
 * and the Government shall have only "Restricted Rights" as defined
 * in Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing,
 * the authors grant the U.S. Government and other acting in its behalf
 * permission to use and distribute the software in accordance with the
 * terms specified in this license.
 *
 * CmdLineParser is a concrete class whose "parse" method
 * takes a command String array and a target Object, and
 * attempts to parse the command Strings to match the name
 * and parameter types of a public method of the target Object.
 * If a match is found, then parse calls the Object method.
 * Also method "isCommand" tests for a valid command in the
 * target Object.
 *
 * CmdLineParser's parse and isCommand methods are synchronized,
 * so that only one client command can be processed against a
 * target Object at one time. Some clients of CmdLineParser may
 * bypass CmdLineParser and call a target object directly for
 * some methods known at compile time. In this case, the client
 * can synchonize using the CmdLineParser object as a lock if the
 * client does so consistently.

 @author Dale Parson
 	 Bell Labs Innovations for Lucent Technologies
	 dparson@lucent.com
*/

public class CmdLineParser {
    final static String cmdPrefix = "command_"; // command name prefix
    final static String optPrefix = "optional_"; // list of optional parms
    final static String usePrefix = "usage_"; // optional usage String
    private Object target ;		// command target
    private Class tclass ;		// target's Class
    private int nummethods ;		// number of command methods
    private static class overload {	// there may be > 1 given method name
	public String command ;		// textual command name
	public Method meth ;		// current method
	public overload next ;		// next method with same name
	public String usage ;		// usage string, if any, for method
	public int numparms ;		// number of method parameters
	public int[] options;		// optional parameter positions, if any
    } ;
    private static class matchflag {	// need a boolean reference parameter
	public boolean matched ;	// String matched to method
	public String errmsg ;		// individual error messages
    } ;
    private Hashtable methtab ;		// lookup set of overloaded methods
    /**
     * CmdLineParser constructor takes the target object of the
     * command calls and stores a reference to it. 
     *
     * @param target	The target Object to be called by parse.
     * parse inspects target for a matching method.
    */
    public CmdLineParser(Object tgt) {
	target = tgt ;
	tclass = target.getClass();
	nummethods = 0 ;
	// java.lang.reflect supports direct lookup of a method only
	// when all parameter types are known in advance; otherwise it
	// supports only linear search through method names; here
	// we speed command processing by organizing lookup by method
	// name without parameter types, parameters matched by command args
	methtab = new Hashtable();
	Method[] cmethods = tclass.getMethods();
	int ixm ;
	int prel = cmdPrefix.length() ;
	for (ixm = 0 ; ixm < cmethods.length ; ixm++) {
	    String mname = cmethods[ixm].getName();
	    if (mname.startsWith(cmdPrefix)) {
		// a public command, strip prefix & store link to it
		String cmdname = mname.substring(prel);
		overload newone = new overload();
		newone.command = cmdname ;
		newone.meth = cmethods[ixm] ;
		// get gets former front of linked list, may be null
		newone.next = (overload) methtab.get(cmdname);
		newone.usage = null ;
		newone.numparms = newone.meth.getParameterTypes().length ;
		newone.options = null ;
		String uname = usePrefix + cmdname ;
		Field ufld ;
		try {
		    ufld = tclass.getField(uname);
		} catch (NoSuchFieldException e) {
		    ufld = null ;
		}
		if (ufld != null) {
		    try {
			Object uval = ufld.get(target);
			if (uval != null && (uval instanceof String)) {
			    newone.usage = (String) uval ;
			}
		    } catch (IllegalArgumentException e) {
		    } catch (IllegalAccessException e) {}
		}
		String oname = optPrefix + cmdname + "_" + newone.numparms ;
		Field ofld ;
		try {
		    ofld = tclass.getField(oname);
		    Class otype = ofld.getType();
		    Class basetype = otype.getComponentType();
		    if (basetype == int.class) {
			newone.options = (int []) ofld.get(target);
		    }
		} catch (NoSuchFieldException e) {
		} catch (IllegalArgumentException e) {
		} catch (IllegalAccessException e) {}
		// put at front of linked list
		methtab.put(cmdname,newone);
		nummethods++ ;
	    }
	}
    }
    /**
     * parse treats the first String in its argv array as
     * a user-entered command and the subsequent Strings as
     * command arguments. parse uses reflection to look for
     * a method matching the command name within the target
     * Object. If it finds a method name of "command_COMMAND"
     * where COMMAND is String argv[0], then parse attempts
     * to match the remaining argv[] Strings to the method
     * parameters. A target Object can contain an optional
     * static int array "optional_COMMAND_N", where N is the
     * decimal number of parameters for the (possibly overloaded)
     * target method, that lists parameter positions (starting at 0,
     * in increasing order) for that method that may be null.
     * parse will substitute null for an optional parameter when
     * necessary to make the match work; only non-primitive
     * parameters (i.e., derived from Object) may be optional.
     * Finally, if the last parameter of the target method is
     * of type "String[]", then parse will place any optional
     * trailing arguments into this trailing array.
     *
     * A target Object can contain an optional static String
     * "usage_COMMAND" that gives a usage string for COMMAND.
     *
     * @param argv	The command line as entered by the user.
     * argv[0] appended to String "command_" must match
     * a target method name. Subsequent arguments must textually
     * match one of the atomic (non-Object) Java types (boolean,
     * char, byte, short, int, long, float, or double),
     * or one of their standard wrapper types (Boolean,
     * Character, Byte, Short, Integer, Long, Float, or Double),
     * or String, or an array of String,
     * or a TextListParser (see parameter "lister"), or any
     * class with a static valueOf function that takes a String
     * argument and returns an object of the required type.
     * A parameter can also be a class with a static valueOf
     * function that takes the full command line String []
     * along with a CmdLineParser.Counter object holding the 
     * offset into the command line String[] at which to consume
     * tokens. This valueOf can consume zero or more strings from
     * the String[], starting at the CmdLineParser.Counter offset. This
     * version of valueOf constructs an object & returns
     * the number of Strings actually consumed (>= 0)
     * in the CmdLineParser.Counter.
     *
     * parse converts these arguments into their corresponding
     * Object types by consulting target method parameters to
     * determine expected type.
     *
     * @param lister	An object whose SplitList function takes
     * a String, tokensizes it into a String array, and returns
     * this array. A lister is needed when a target method parameter
     * is of type String array. The need for nested tokenization is
     * context-sensitive, based on the method being matched. The
     * rules for nested tokenization depend on the extension
     * language being used, hence the need for a callable lister.
     * A TextListParser also contains a MergeList method that
     * glues a String array into a single String, the inverse
     * of SplitList. A command method can use a parameter of type
     * TextListParser when the method needs to construct a nested
     * return String. parse simply passes its lister parameter
     * to any target method TextListParser parameter.
     *
     * @exception cmdException	Thrown on usage errors and bad formats.
     *
     * @exception cmdUnknownMethod Thrown on request for an unknown
     * method (unknown argv[0]).
     *
     * @return	Return String from target Object method. Target
     * can return any primitive type, primitive wrapper type or
     * an array of Strings, which parse can convert to a String.
     * 
    */
    public synchronized String parse(String[] argv, TextListParser lister)
	throws cmdException, cmdUnknownMethod {
	boolean isName = false ;
	String usage = null, result ;
	matchflag isOK = new matchflag() ;
	isOK.matched = false ;
	overload curmeth ;
	for (curmeth = (overload) methtab.get(argv[0]) ; curmeth != null ;
		curmeth = curmeth.next) {
	    isName = true ;
	    usage = curmeth.usage ;
	    Class[] ptypes = curmeth.meth.getParameterTypes() ;
	    Object[] args = new Object[ptypes.length] ;
	    isOK.errmsg = "" ;
	    result = apply(curmeth.meth,ptypes,argv,args,lister,
			    curmeth.options,0,1,isOK) ;
	    if (isOK.matched) {
		return(result);
	    }
	}
	if (isName) {
	    String umsg ;
	    // found a name match, report a usage string
	    if (usage != null) {
		umsg = "usage: " + usage ;
	    } else {
		umsg = "invalid usage : " + argv[0];
	    }
	    if (isOK.errmsg.length() > 0) {
		umsg = umsg + ", " + isOK.errmsg ;
	    }
	    throw new cmdException(umsg);
	}
	throw new cmdUnknownMethod("unknown command: " + argv[0]);
    }

    /**
    * Counter is a helper type used for passing command-line
    * position into, and number of tokens consumed out of a
    * valueOf(String[]. CmdLineParser.Counter) static factory
    * method. See parse.
    */
    public static class Counter {
	private int val ;
	public Counter () {
	    val = 0 ;
	}
	public Counter (int i) {
	    val = i ;
	}
	public int getInt() {
	    return(val);
	}
	public void setInt(int i) {
	    val = i ;
	}
    }

    /**
     * isCommand tests whether parameter command is a target method
     *
     * @param command	The proposed command name.
     *
     * @return	true if command is a public command_ method, else false.
     *
    */
    public synchronized boolean isCommand(String command) {
	return(((overload) methtab.get(command)) != null);
    }

    /**
     * getMethodCount returns the number of public command methods
     * exposed by the target object.
     *
     * @return	Command method count.
     *
    */
    public int getMethodCount() {
	return(nummethods);
    }

    /**
     * method_N data structure allows getMethodNames to return
     * name/arity pairs
    */
    public static class method_N { // data struct to hold method name + arity
	String name ;
	int arity ;
	public String getName() {
	    return(name);
	}
	public int getArity() {
	    return(arity);
	}
    }

    /**
     * getMethodNames returns an array of command names & their arities.
     *
     * @return	Array of method_N structures with name-arity number pairs.
    */
    public method_N[] getMethodNames () {
	method_N[] retval ;
	retval = new method_N[nummethods] ;
	Enumeration hashelems = methtab.elements() ;
	int ix = 0 ;
	while (hashelems.hasMoreElements()) {
	    overload nextm = (overload) hashelems.nextElement();
	    while (nextm != null) {
		retval[ix] = new method_N();
		retval[ix].name = nextm.command ;
		retval[ix++].arity = nextm.numparms ;
		nextm = nextm.next ;
	    }
	}
	return(retval);
    }

    /**
     * Helper method apply tries to apply a method to the target Object,
     * by matching command Strings to parameter types, populating an
     * array of args Objects as it goes. It uses recursion and
     * backtracking in matching command Strings to method parameters.
     *
     * @parm method	The method to try to apply.
     *
     * *parm ptypes	The method parameter types.
     *
     * @parm argv	Command Strings, argv[1] is the first argument.
     *
     * @parm args	Array of actual arguments, populated by apply.
     *
     * @parm lister	See lister from parse.
     *
     * @parm options	Array holding optional-null parameters, may be null.
     *
     * @parm matchix	Number of parms matched so far, starts at 0.
     *
     * @parm argix	Next command position to be matched.
     *
     * @parm isMatch	Boolean flag becomes et to true on successful apply.
     *
     * @return		Return String from target method on successful
     *			apply (i.e., isMatch.mathed == true), else "".
     *
     * @exception cmdException	Thrown on usage errors and bad formats.
     *
    */
    private String apply(Method method,Class[] ptypes,String[] argv,
	Object[] args,TextListParser lister,int[] options,
	int matchix,int argix, matchflag isMatch) throws cmdException {
	String retval = "" ;
	Object retobj = null ;
	isMatch.matched = false ;
	// Algorithm needs to fill all parameters, so the terminating
	// condition is that all parameters have been matched.
	if (matchix == ptypes.length) {
	    if (argix == argv.length) {
		// Any unconsumed text args fails on usage.
		try {
		    retobj = method.invoke(target,args);
		} catch (InvocationTargetException e) {
		    // The called app function threw an exception.
		    Throwable texc = e.getTargetException();
		    if (texc == null || texc.getMessage() == null) {
			throw new cmdException(argv[0]
			    + ": unknown application error");
		    } else {
			throw new cmdException(argv[0] + ": "
			    + texc.getMessage());
		    }
		} catch (IllegalArgumentException e) {
		    throw new cmdException("illegal argument error: "
			+ argv[0] + ": " + e.getMessage());
		} catch (Exception e) {
		    // Probably a parm mismatch in parse() or apply().
		    throw new cmdException("execution error: "
			+ argv[0] + ": " + e.getMessage());
		}
		isMatch.matched = true ;
		// format the return value as a String
		Class rettype = method.getReturnType();
		if (retobj == null || rettype == void.class
			|| rettype == Void.class
			|| (retobj instanceof Void)) {
		    retval = "" ;
		} else {
		    retval = retobj.toString();
		}
	    }
	} else {
	    args[matchix] = null ;
	    int consumed = 1 ;		// number of text args consumed
	    // Try to match next parm with next arg, or with null, or w. rest.
	    // First try matching next parm with next arg.,
	    // no arg. needed for a TextListParser.
	    if (argix < argv.length
		    || ptypes[matchix] == TextListParser.class) {
		try {
		    if (ptypes[matchix] == boolean.class
			    || ptypes[matchix] == Boolean.class) {
			if (argv[argix].equalsIgnoreCase("true")
				|| argv[argix].equalsIgnoreCase("false")
				|| argv[argix].equals("")) {
			    // treat "" as false
			    args[matchix] = Boolean.valueOf(argv[argix]);
			}
		    } else if (ptypes[matchix] == char.class
			    || ptypes[matchix] == Character.class) {
			if (argv[argix].length() > 0) {
			    // use the first character
			    args[matchix] =
				new Character(argv[argix].charAt(0));
			}
		    } else if (ptypes[matchix] == byte.class
			    || ptypes[matchix] == Byte.class) {
			args[matchix] = Byte.decode(argv[argix]);
		    } else if (ptypes[matchix] == short.class
			    || ptypes[matchix] == Short.class) {
			args[matchix] = Short.decode(argv[argix]);
		    } else if (ptypes[matchix] == int.class
			    || ptypes[matchix] == Integer.class) {
			args[matchix] = Integer.decode(argv[argix]);
		    } else if (ptypes[matchix] == long.class
			    || ptypes[matchix] == Long.class) {
			args[matchix] = Long.decode(argv[argix]);
		    } else if (ptypes[matchix] == float.class
			    || ptypes[matchix] == Float.class) {
			args[matchix] = new Float(argv[argix]);
		    } else if (ptypes[matchix] == double.class
			    || ptypes[matchix] == Double.class) {
			args[matchix] = new Double(argv[argix]);
		    /* BigDecimal & BigInteger commented out for now,
		       to avoid pulling in typically unwanted code
		    } else if (ptypes[matchix] == BigDecimal.class) {
			args[matchix] = new BigDecimal(argv[argix]);
		    } else if (ptypes[matchix] == BigInteger.class) {
			args[matchix] = new BigInteger(argv[argix]);
		    */
		    } else if (ptypes[matchix] == String.class) {
			args[matchix] = argv[argix] ;
		    } else if (ptypes[matchix] == TextListParser.class) {
			// application method uses TextListParser to glue
			// String arrays into structured list strings
			args[matchix] = lister ;
			consumed = 0 ;
		    } else if (ptypes[matchix].isArray()
			    && ptypes[matchix].getComponentType()
				== String.class) {
			if (lister.isList(argv[argix])) {
			    args[matchix] = lister.SplitList(argv[argix]);
			}
		    } else {
			// unknown type, look for correct valueOf method
			// First try to match exactly on String parm type,
			// but if that doesn't work, use Object and let
			// toString handle the conversion.
			Class[] oparms = new Class[1] ;
			Class[] sparms = new Class[1] ;
			oparms[0] = Object.class ;
			sparms[0] = String.class ;
			Method mtry = null ;
			try {
			    mtry = ptypes[matchix].getMethod("valueOf",sparms);
			} catch (NoSuchMethodException e) {
			} catch (SecurityException e) {}
			if (mtry == null) {
			    try {
				mtry =
				    ptypes[matchix].getMethod("valueOf",oparms);
			    } catch (NoSuchMethodException e) {
			    } catch (SecurityException e) {}
			}
			if (mtry != null
				&& mtry.getReturnType() == ptypes[matchix]
				&& Modifier.isStatic(mtry.getModifiers())) {
			    try {
				Object[] stringargs = new Object[1];
				stringargs[0] = argv[argix] ;
				args[matchix] = mtry.invoke(null,stringargs);
			    } catch (Exception e) {}
			}
			if (args[matchix] == null) {
			    // try valueOf(String [], Counter)
			    Class[] vparms = new Class[2] ;
			    vparms[0] = argv.getClass();
			    vparms[1] = CmdLineParser.Counter.class ;
			    mtry = null ;
			    try {
				mtry = ptypes[matchix].getMethod("valueOf",
				    vparms);
			    } catch (NoSuchMethodException e) {
			    } catch (SecurityException e) {}
			    if (mtry != null
				    && mtry.getReturnType() == ptypes[matchix]
				    && Modifier.isStatic(mtry.getModifiers())) {
				try {
				    Object[] twoargs = new Object[2];
				    Counter Cmdix = new Counter(argix);
				    twoargs[0] = argv ;
				    twoargs[1] = Cmdix ;
				    args[matchix] = mtry.invoke(null,twoargs);
				    if (args[matchix] != null) {
					consumed = Cmdix.getInt();
				    }
				} catch (Exception e) {}
			    }
			}
		    }
		} catch (NumberFormatException e) {
		    args[matchix] = null ;
		    isMatch.errmsg = "numeric format error on \""
			+ argv[argix] + "\"" ;
		} catch (Exception e) {
		    args[matchix] = null ;
		    // simply let the match fail on usage.
		}
		if (args[matchix] != null) {
		    // Matching has filled this parameter with a non-null value,
		    // continue through remaining arguments.
		    retval = apply(method,ptypes,argv,args,lister,
				options,matchix+1,argix+consumed,isMatch);
		}
	    }
	    // We may have run out of command line args, or they may not
	    // have satisfied the match. If the match is unsatisfied,
	    // try an optional parameter.
	    if ((! isMatch.matched) && isOptional(matchix,options)
		    && (! ptypes[matchix].isPrimitive())) {
		args[matchix] = null ;
		retval = apply(method,ptypes,argv,args,lister,
				options,matchix+1,argix,isMatch);
	    }
	    // If no match has succeeded and we are at the last parm,
	    // of type String[], and their are arguments left, copy them!
	    // If no command line strings remain, an empty list is made.
	    if ((! isMatch.matched) && matchix == (ptypes.length-1)
		    && ptypes[matchix].isArray()
		    && ptypes[matchix].getComponentType() == String.class
		    && argix <= argv.length) {
		consumed = argv.length - argix ;
		int fromix = argix ;
		int toix = 0 ;
		String[] rest = new String[consumed];
		while (toix < consumed) {
		    rest[toix++] = argv[fromix++];
		}
		args[matchix] = rest ;
		retval = apply(method,ptypes,argv,args,lister,
				options,matchix+1,fromix,isMatch);
	    }
	}
	return(retval);
    }

    /**
     * isOptional is a private helper that determines whether
     * parameter parmix appears in the options array of optional
     * parameter positions
    */
    private boolean isOptional(int parmix, int[] options) {
	int i ;
	for (i = 0 ; options != null && i < options.length ; i++) {
	    if (options[i] == parmix) {
		return(true);
	    }
	}
	return(false);
    }
}
