/* *****************************************************************************

 * $Novell: ApplicationArguments.java,v 1.5 2003/01/28 16:52:10 $

 * Copyright (c) 2001 Novell, Inc. All Rights Reserved.

 *

 * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND

 * TREATIES. USE AND REDISTRIBUTION OF THIS WORK IS SUBJECT TO THE LICENSE

 * AGREEMENT ACCOMPANYING THE SOFTWARE DEVELOPMENT KIT (SDK) THAT CONTAINS

 * THIS WORK. PURSUANT TO THE SDK LICENSE AGREEMENT, NOVELL HEREBY GRANTS TO

 * DEVELOPER A ROYALTY-FREE, NON-EXCLUSIVE LICENSE TO INCLUDE NOVELL'S SAMPLE

 * CODE IN ITS PRODUCT. NOVELL GRANTS DEVELOPER WORLDWIDE DISTRIBUTION RIGHTS

 * TO MARKET, DISTRIBUTE, OR SELL NOVELL'S SAMPLE CODE AS A COMPONENT OF

 * DEVELOPER'S PRODUCTS. NOVELL SHALL HAVE NO OBLIGATIONS TO DEVELOPER OR

 * DEVELOPER'S CUSTOMERS WITH RESPECT TO THIS CODE.

 ******************************************************************************/

package arguments;



import java.util.ArrayList;

import java.text.ParseException;



/**

 * This class represents a group of command-line arguments and determines

 * the semantics by which they are parsed.

 * <p>

 * Arguments are of two types, option arguments - i.e., the meaning of the

 * argument is derermined by an option letter, and positional arguments - i.e.,

 * for arguments that have no option letter, the meaning is determined by

 * by its position on the commannd line.

 * <p>

 * Arguments are added to an instance of this class one at a time.

 * The order they are added determines the order in which they are displayed

 * on the usage string, and also determines the meaning when parsing positional

 * arguments.

 *<p>

 * The characteristics of each Argument added determines how it is parsed.

 * The following characteristics determine parsing semantics:

 *<br>

 * - Meaning determined by option or position - an option requires an option

 *   letter. Arguments that are options can be specified in any order on the

 *   command line.

 *   If no option letter is given, meaning, i.e., its value, is determined by

 *   its position on the command line.

 *<br>

 * - argument type - whether the argument type is Boolean, Integer, or String.

 *<br>

 * - required - whether the argument is optional or or required.  Required

 *   arguments must be present.

 *<br>

 * - single or multi-valued - whether the argument can have more than one value.

 * <p>

 * The parsing of command line arguments obeys the following rules:

 * <br>

 * - Options begin with a dash (-) and must be followed by at least one

 *   option letter.  A dash with no option letter is parsed as the value "-".

 *<p>

 * Boolean options can be strung together, i.e. -abc is the same as -a -b -c.

 *<p>

 * String and Integer options must be followed by a value.  The value may

 * be specified on the command line immediately following the option or

 * be separated from the option by a space, i.e. -ddog is the same as -d dog.

 * A String or Integer option can be strung together with a boolean option,

 * as long as it is the last option in the grouping, i.e. -abcddog or -abcd dog

 * is the same as -a -b -c -d dog.

 *<p>

 * Boolean option have a single value, and are always optional arguments.

 *<p>

 * Multiple values for String and Integer options are specified by repeating

 * the option followed by the value, i.e. -d dog -d cat -dchick give the

 * option d the three values for the -d option of dog, cat, and chick.

 *<br>

 * Values for positional arguments are identified as a String or Integer without

 * a leading dash and where no option value is expected.  For example, using

 * the previously defined Boolean options of -a, -b, & -c which have default

 * values of false, as well as the String option -d,

 * a command line specifying the following:

 *<br><br>

 * "-a please -b help -ddog me -d cat - -dchick now

 *<br><br.

 * is parsed as follows:

 * <br>

 * -a option true

 * <br>

 * -b option true

 * <br>

 * -c option false (default)

 * <br>

 * -d option - three values, dog, cat, & chick

 * <br>

 * positional parameter 1, please

 * <br>

 * positional parameter 2, help

 * <br>

 * positional parameter 3, me

 * <br>

 * positional parameter 4, -

 * <br>

 * positional parameter 5, now

 *<p>

 * Optional parameters (including Boolean) must have a default value specified

 * when added to this object.  Required parameters have no default value.

 * The default value is used when the argument is not specified on the

 * command line.

 */



public class ApplicationArguments

{

    private ArrayList appArguments;

    private String applicationName = "program";



    /**

     * Constructor to create an ApplicationArguments class.

     */

    public ApplicationArguments()

    {

        appArguments = new ArrayList();

        return;

    }



    /**

     * Constructor to create an ApplicationArguments class with application

     * name.

     *

     * @param appName the application name for the usage String

     */

    public ApplicationArguments(String appName)

    {

        applicationName = appName;

        appArguments = new ArrayList();

        return;

    }



    /**

     * Constructor to create an ApplicationArguments class with initial

     * capacity.

     *

     * @param initialCapacity the initial size of the array that holds the

     *                        commandline arguments added.

     */

    public ApplicationArguments( int initialCapacity)

    {

        appArguments = new ArrayList(initialCapacity);

        return;

    }



    /**

     * Constructor to create an ApplicationArguments class with

     * application name and initial capacity.

     *

     * @param appName the application name for the usage String

     *

     * @param initialCapacity the initial size of the array that holds the

     *                        commandline arguments added.

     */

    public ApplicationArguments( String appName, int initialCapacity)

    {

        applicationName = appName;

        appArguments = new ArrayList(initialCapacity);

        return;

    }



    /**

     * Add an Argument to this application

     */

    public boolean add( Argument argument)

    {

        return appArguments.add( argument);

    }



    /**

     * Returns true if the named argument option was specified on the

     * command line

     */

    public boolean hasArgument( char argumentLetter)

        throws NoSuchFieldException

    {

        return getArgument(argumentLetter).getValueCount() > 0;

    }



    /**

     * Return the number of arguments registered for this applicaton.

     */

    public int getNumberOfArguments()

    {

        return appArguments.size();

    }



    /**

     * Return the number of arguments registered for this applicaton.

     */

    public int getNumberOfPositionalArguments()

    {

        int count = 0;

        Argument arg = null;

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.isPositional()) {

                count += arg.getValueCount(); // Count number of values

            }

        }

        return count;

    }



    /**

     * Return the argument class representing this option letter

     *

     * @param optionLetter the option letter

     *

     * @return the Argument class for the specified option letter

     */

    public Argument getArgument( char optionLetter)

        throws NoSuchFieldException

    {

        Argument arg = null;

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.getArgumentLetter() == optionLetter) {

                break;

            }

            arg = null;

        }

        if( arg == null) {

            throw new NoSuchFieldException("Argument \"" + optionLetter +

                    "\" is not defined in this application");

        }

        return arg;

    }



    /**

     * Return the argument class for the Nth positional argument

     * where getArgument(1) will return the first one.

     *

     * @param position the positional argument number

     *

     * @return the Argument class representing the positional argument

     */

    public Argument getArgument( int position)

        throws NoSuchFieldException

    {

        int count = 1;

        Argument arg = null;

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.isPositional()) {

                if( count == position) {

                    break;

                }

                count++;

            }

            arg = null;

        }

        if( arg == null) {

            throw new NoSuchFieldException("Argument at position " + position +

                    " is not defined in this application");

        }

        return arg;

    }



    /**

     * returns a String with the default value for the given Argument class,

     * formatted for the usage string.

     *

     * @param arg the Argument class

     */

    private String defString( Argument arg)

    {

        if( arg.isRequired()) {

            return " - Required Argument";

        } else

        if( arg.isString() ) {

            return " - Default=" + "\"" +

                    (String)arg.getDefaultValue() + "\"";

        } else

        if( arg.isInteger() ) {

            return " - Default=" +

                    ((Integer)arg.getDefaultValue()).intValue();

        } else {

            return " - Default=" +

                    ((Boolean)arg.getDefaultValue()).booleanValue();

        }

    }



    /**

     * Returns the usage string including the error message supplied.

     */

    public String usage( String message)

    {

        String option;

        Argument arg = null;

        String usageString = "";



        // Print error string

        if( message != null) {

            usageString = usageString + "Error: " + message + "\n\n";

        }



        // Display usage line

        usageString = usageString + "Usage: " + applicationName;



        // usage line: Display any required boolean options

        String dash = " -";

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.isBoolean() && arg.isRequired()) {

                usageString = usageString + dash + arg.getArgumentLetter();

                dash = "";

            }

        }

        if( dash == "") {

            usageString = usageString + " ";

        }



        // usage line: Display any optional boolean options

        dash = " [ -";

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.isBoolean() && ! arg.isRequired()) {

                usageString = usageString + dash + arg.getArgumentLetter();

                dash = "";

            }

        }

        if( dash == "") {

            usageString = usageString + " ] ";

        }



        // usage line: Display the rest of the non positional arguments

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( ! arg.isBoolean() && ! arg.isPositional()) {

                if( ! arg.isRequired()) {

                    usageString = usageString + "[ ";

                }

                usageString = usageString + "-" + arg.getArgumentLetter();

                usageString = usageString + " <" + arg.getName() + ">";

                if( ! arg.isRequired()) {

                    usageString = usageString + " ] ";

                } else {

                    usageString = usageString + " ";

                }

            }

        }



        // usage line: Display the positional arguments

        String endPositional = "";

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.isPositional()) {

                if( ! arg.isRequired()) {

                    usageString = usageString + " [ ";

                }

                usageString = usageString + " <" + arg.getName() + ">";

                if( ! arg.isRequired()) {

                    endPositional = endPositional + " ]";

                } else {

                    endPositional = endPositional + " ";

                }

            }

        }

        usageString = usageString + endPositional;



        // Now display a line for each argument with its meaning

        usageString = usageString + "\n";

        for( int i = 0; i < appArguments.size(); i++) {

            arg = (Argument)appArguments.get(i);

            if( arg.isPositional()) {

                if( arg.getName().length() < 7) {

                    option = "<" + arg.getName() + ">" +

                        "        ".substring(1,9-arg.getName().length());

                } else {

                    option = "<" + arg.getName() + "> ";

                }

            } else {

                option = "    -" + arg.getArgumentLetter() + "    ";

            }

            usageString = usageString + "\n" + option + arg.getDescription();

            usageString = usageString + defString(arg);

        }

        return usageString;

    }



    /**

     * Parse the specified arguments using the arguments added

     * to this class to determine the parsing rules.

     *

     * @param arguments an array containing the command arguments, one

     * token per String in the array.

     */

    public void parse(String[] arguments)

        throws ParseException

    {

        int position = 0;   // Positional argument number - 1 is the first

        boolean typeBool = false;   // true option is boolean, otherwise false

        char letter;        // The option letter being processed

        String value;       // Value of the argument

        Argument arg;       // Argument class representing option

        int idx = 0;        // Position of index in argument



        for( int i = 0; i < arguments.length; i++) {



            idx = 0;

            value = arguments[i];

            // - options must be at least two chars in length

            // - if only '-' is specified, just send it through as positional

            if((value.length() > 1) && (value.charAt(0) == '-')) {

                idx = 1;

                // Found an option - begins with dash

                for( ; idx < value.length(); idx++) {

                    letter = value.charAt(idx);

                    try {

                        arg = getArgument(letter);

                    } catch( NoSuchFieldException e) {

                        throw new ParseException("Argument " + (i+1) + ": '-" +

                                letter + "' is an unknown option", i+1);

                    }

                    if( arg.isBoolean()) {

                        if( idx == 1) {

                            arg.add( new Boolean( true), i);

                            typeBool = true; // processing boolean arguments

                            continue;

                        } else {

                            if( typeBool) {

                                arg.add( new Boolean( true),i);

                                continue;

                            } else {

                                // Allow non boolean if last arg in the string

                                if( idx != (value.length() -1)) {

                                    throw new ParseException("Argument " +

                                            (i+1) + ": '-" + letter +

                                            "' must be " +

                                            "specified separately", i+1);

                                }

                            }

                        }

                    }

                    // Don't do else, we process fall through from the above

                    if( ! arg.isBoolean()) {

                        // if option at the end of the string, value is next arg

                        if( idx == (value.length() - 1)) {

                            // Check if any arguments left

                            if( (i+1) == arguments.length) {

                                throw new ParseException("Argument " + (i+1) +

                                        ": '-" + letter + "' must have " +

                                        "a value", i+1);

                            }

                            value = arguments[++i];

                        } else {

                            // Value is the rest of the string

                            value = arguments[i].substring( ++idx);

                        }



                        if( arg.isString()) {

                            arg.add(value, i);

                        } else {

                            // it is an integer

                            try {

                                arg.add( Integer.decode(value), i);

                            } catch( NumberFormatException e) {

                                throw new ParseException("Argument " + (i+1) +

                                    ": '-" + letter + "' does not have" +

                                    "a valid integer value", i+1);

                            }

                        }

                        break;

                    }

                }

            } else {

                // Process positional argument

                position += 1;

                try {

                    arg = getArgument(position);

                } catch( NoSuchFieldException e) {

                    throw new ParseException("Argument " + (i+1) +

                       " Unknown positional argument \"" + value + "\"", i+1);

                }

                arg.add( value, i);



                // If not multivalued, we are done

                if( ! arg.isMultivalued()) {

                    continue;

                }



                // Multivalued, read rest of values

                while( true) {

                    // Multi-valued argument

                    // stop if this is the last argument, i.e. no next argument

                    if( (i+1) == arguments.length) {

                        break;

                    }



                    // if next arg is an option, break

                    // An option must be at least two characters

                    if((arguments[i+1].length() > 1) &&

                            (arguments[i+1].charAt(0) == '-')) {

                        // Go back to outer loop to process option

                        break;

                    }

                    arg.add( arguments[++i], i);

                }

            }

        }



        // Verify that all required arguments have a value

        for( int i = 0; i < appArguments.size(); i++) {

            int count = 0;

            arg = (Argument)appArguments.get(i);

            // Count positional arguments

            if( arg.isPositional()) {

                count++;

            }

            if( arg.isRequired() && (arg.getValueCount() == 0)) {

                if( arg.isPositional()) {

                    throw new ParseException("Positional argument " + count +

                        " (" + arg.getDescription() + ") not specified", i+1);

                } else {

                    throw new ParseException("Required argument '-" +

                        arg.getArgumentLetter() + "' not specified", i+1);

                }

            }

        }

        return;

    }

}

