All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bitbucket.bradleysmithllc.java_cl_parser.CommonsCLILauncher Maven / Gradle / Ivy

Go to download

This parser strives to achieve an elegant, ioc-type interface to make launching command-line projects effortless.

There is a newer version: 3.4.2
Show newest version
package org.bitbucket.bradleysmithllc.java_cl_parser;

import org.apache.commons.cli.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.bitbucket.bradleysmithllc.java_cl_parser.regexp.EscapeFunctionExpression;
import org.bitbucket.bradleysmithllc.java_cl_parser.util.RecursiveMap;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.*;
import java.util.regex.Pattern;

public class CommonsCLILauncher
{
	public static final String HELP_INVALID_USAGE_MESSAGE = "Help is not valid in combination with any other option";
	public static final String HELP_USAGE_MESSAGE = "Help requested";
	public static final String HELP_REQUIRED_MESSAGE = "Help required";
	private static final String [] NULL_ARGS = new String[]{};

	enum data_type
	{
		bool,
		Bool,
		long_integer,
		Long,
		integer,
		Integer,
		string,
		String,
		Object
	}

	enum cardinal_type
	{
		instance,
		array,
		flag
	}

	static final class Data
	{
		data_type type = data_type.bool;
		cardinal_type cardinality = null;
		Method setter;
		CLIOption clioption;
	}

	private static final Map reservedOptionNames = new HashMap();

	static
	{
		reservedOptionNames.put("?", "?");
		reservedOptionNames.put("help", "help");
		reservedOptionNames.put("verbose", "verbose");
		reservedOptionNames.put("v", "v");
	}

	private static final CLIOption help = new CLIOption(){
		public String name()
		{
			return "?";
		}

		public String longName()
		{
			return "help";
		}

		public String description()
		{
			return "Prints this usage description.";
		}

		public boolean required()
		{
			return false;
		}

		public value_type valueType()
		{
			return value_type.not_allowed;
		}

		public int valueCardinality()
		{
			return 0;
		}

		public char valueSeparator()
		{
			return CLIOption.ARRAY_SEPARATOR_DEFAULT;
		}

		public String defaultValue()
		{
			return CLIOption.NULL_STRING_VALUE;
		}

		public String[] enumeratedValues()
		{
			return new String[0];
		}

		public Class annotationType()
		{
			return CLIEntry.class;
		}
	};

	private static final CLIOption verbose = new CLIOption(){
		public String name()
		{
			return "v";
		}

		public String longName()
		{
			return "verbose";
		}

		public String description()
		{
			return "Displays the command line options.";
		}

		public boolean required()
		{
			return false;
		}

		public value_type valueType()
		{
			return value_type.not_allowed;
		}

		public int valueCardinality()
		{
			return 0;
		}

		public char valueSeparator()
		{
			return CLIOption.ARRAY_SEPARATOR_DEFAULT;
		}

		public String defaultValue()
		{
			return CLIOption.NULL_STRING_VALUE;
		}

		public String[] enumeratedValues()
		{
			return new String[0];
		}

		public Class annotationType()
		{
			return CLIEntry.class;
		}
	};

	private static final CLIOption [] impliedOptions = new CLIOption[] {
			help,
			verbose
	};

	public static Object mainClean(String[] argv)
	{
		try
		{
			return main(argv);
		}
		catch (UsageException e)
		{
			if (e.getMessage() != HELP_USAGE_MESSAGE)
			{
				e.printStackTrace(System.out);

				System.out.println(e.getFormattedUsageStatement());
			}
			else
			{
				// this is not an error
				System.out.println(e.getFormattedUsageStatement());

				return ShutdownHandler.instance().shutdown(0);
			}
		}
		catch (InvalidCLIEntryException e)
		{
			e.printStackTrace(System.err);
		}
		catch (MissingCLIEntryException e)
		{
			e.printStackTrace(System.err);
		}

		return ShutdownHandler.instance().shutdown(-1);
	}

	public static Object main(String[] argv) throws InvalidCLIEntryException, UsageException, MissingCLIEntryException
	{
		// the protocol is system property, then call stack
		String val = System.getProperty("CLIMain");

		if (val != null)
		{
			Object launchable = resolveLaunchable(val);
			return mainWithInstance(launchable, argv);
		}

		// determine the appropriate caller
		StackTraceElement[] st = Thread.currentThread().getStackTrace();

		// ignoring the last element, which will be the current class, move up the chain
		// and grab the first CLIEntry you find
		for (int i = 1; i < st.length; i++)
		{
			String name = st[i].getClassName();

			try
			{
				Object launchable = resolveLaunchable(name);
				return mainWithInstance(launchable, argv);
			}
			catch(MissingCLIEntryException exc)
			{
				// go on to try the next one
			}
		}

		// fail loudly
		throw new MissingCLIEntryException("No acceptable CLIEntry found on the stack.");
	}

	private static Object resolveLaunchable(String name)
			throws InvalidCLIEntryException, MissingCLIEntryException, UsageException
	{
		try
		{
			Class cl = Thread.currentThread().getContextClassLoader().loadClass(name);

			// look for the marker annotation
			CLIEntry annot = cl.getAnnotation(CLIEntry.class);

			if (annot != null)
			{
				// try this one
				// next requirement is exactly one CLIMain annotation
				int mains = 0;
				for (Method method : cl.getMethods())
				{
					CLIMain climain = method.getAnnotation(CLIMain.class);

					if (climain != null)
					{
						mains++;
					}
				}

				if (mains == 0)
				{
					throw new InvalidCLIEntryException("Class [" + cl + "] has a CLIEntry annotation, but no methods annotated with CLIMain", InvalidCLIEntryException.invalid_cause.missing_cli_main);
				}
				else if (mains > 1)
				{
					throw new InvalidCLIEntryException("Class [" + cl + "] has a CLIEntry annotation, but more than one method annotated with CLIMain", InvalidCLIEntryException.invalid_cause.redundant_cli_main);
				}

				// bail - our work is done
				return cl.newInstance();
			}

			throw new MissingCLIEntryException();
		}
		catch (ClassNotFoundException e)
		{
			// this class is not available to us - flag it as missing
			throw new MissingCLIEntryException(e);
		}
		catch (InstantiationException e)
		{
			throw new InvalidCLIEntryException(e, InvalidCLIEntryException.invalid_cause.class_access_error);
		}
		catch (IllegalAccessException e)
		{
			throw new InvalidCLIEntryException(e, InvalidCLIEntryException.invalid_cause.class_access_error);
		}
	}

	public static Object mainWithInstanceClean(Object obj, String[] argv)
	{
		try
		{
			return mainWithInstance(obj, argv);
		}
		catch (UsageException e)
		{
			if (e.getMessage() != HELP_USAGE_MESSAGE)
			{
				e.printStackTrace(System.out);

				System.out.println(e.getFormattedUsageStatement());
			}
			else
			{
				// this is not an error
				System.out.println(e.getFormattedUsageStatement());

				return ShutdownHandler.instance().shutdown(0);
			}
		}
		catch (InvalidCLIEntryException e)
		{
			e.printStackTrace(System.out);
		}

		return ShutdownHandler.instance().shutdown(-1);
	}

	public static Object mainWithInstance(Object obj, String[] argv)
			throws UsageException, InvalidCLIEntryException
	{
		if (argv == null)
		{
			argv = NULL_ARGS;
		}

		Options options = new Options();
		Map instanceMap = new HashMap();
		Map valueMap = new HashMap();
		Map displayValueMap = new HashMap();
		List typeList = new ArrayList();

		Data d = new Data();
		d.clioption = help;
		d.cardinality = cardinal_type.flag;

		typeList.add(d);

		d = new Data();
		d.clioption = verbose;
		d.cardinality = cardinal_type.flag;

		typeList.add(d);

		// look through the annotations for methods of this class, and create options for each
		Method[] methList = obj.getClass().getMethods();

		for (int i = 0; i < methList.length; i++)
		{
			CLIOption annot = methList[i].getAnnotation(CLIOption.class);

			if (annot != null)
			{
				// validate that this parameter is not one of the reserved options
				if (reservedOptionNames.containsKey(annot.name()) || reservedOptionNames.containsKey(annot.longName()))
				{
					throw new InvalidCLIEntryException("Option name [" + annot.name() + "] or long name [" + annot.longName() + "] is reserved", InvalidCLIEntryException.invalid_cause.bad_options);
				}

				// validate some basic bad configurations
				assertValid(annot);
				// validate that this is a method which accepts a single parameter
				Class[] parameterTypes = methList[i].getParameterTypes();

				Data data = new Data();
				data.setter = methList[i];
				data.clioption = annot;

				if (instanceMap.containsKey(annot.name()))
				{
					throw new InvalidCLIEntryException("Option [" + annot.name() + "] specified more than once", InvalidCLIEntryException.invalid_cause.bad_options);
				}
				else if (instanceMap.containsKey(annot.longName()))
				{
					throw new InvalidCLIEntryException("Option [" + annot.longName() + "] specified more than once", InvalidCLIEntryException.invalid_cause.bad_options);
				}

				instanceMap.put(annot.name(), "");

				if (!annot.longName().equals(CLIOption.NULL_STRING_VALUE))
				{
					instanceMap.put(annot.longName(), "");
				}

				typeList.add(data);

				if (parameterTypes == null || parameterTypes.length != 1)
				{
					throw new InvalidCLIEntryException("Setter for option ["
						+ annot.name()
						+ "] does not accept exactly one parameter: "
						+ methList[i], InvalidCLIEntryException.invalid_cause.bad_options);
				}

				// if the cardinality is anything other than 1, the parameter must be an array type
				Class parameterType = parameterTypes[0];
				Class arrayComponentType = parameterType.isArray() ? parameterType.getComponentType() : parameterType;

				if (parameterType.isArray())
				{
					data.cardinality = cardinal_type.array;
				}
				else
				{
					data.cardinality = cardinal_type.instance;
				}

				// validate that the type of the parameter is within bounds
				if (arrayComponentType.equals(Object.class))
				{
					data.type = data_type.Object;
				}
				else if (arrayComponentType.equals(String.class))
				{
					data.type = data_type.String;
				}
				else if (arrayComponentType.equals(boolean.class))
				{
					data.type = data_type.bool;

					if (data.cardinality != cardinal_type.array)
					{
						data.cardinality = cardinal_type.flag;
					}
				}
				else if (arrayComponentType.equals(Boolean.class))
				{
					data.type = data_type.Bool;

					if (data.cardinality != cardinal_type.array)
					{
						data.cardinality = cardinal_type.flag;
					}
				}
				else if (arrayComponentType.equals(int.class))
				{
					data.type = data_type.integer;
				}
				else if (arrayComponentType.equals(Integer.class))
				{
					data.type = data_type.Integer;
				}
				else if (arrayComponentType.equals(long.class))
				{
					data.type = data_type.long_integer;
				}
				else if (arrayComponentType.equals(Long.class))
				{
					data.type = data_type.Long;
				}

				if (data.type == null)
				{
					throw new InvalidCLIEntryException("Setter for option ["
							+ annot.name()
							+ "] does not accept a valid parameter type: "
							+ methList[i], InvalidCLIEntryException.invalid_cause.bad_options);
				}

				if (annot.valueCardinality() == 1)
				{
					if (data.cardinality == cardinal_type.array)
					{
						throw new InvalidCLIEntryException("Setter for option ["
								+ annot.name()
								+ "] must not use an array type: "
								+ methList[i], InvalidCLIEntryException.invalid_cause.bad_options);
					}
				}
				else
				{
					if (data.cardinality != cardinal_type.array)
					{
						throw new InvalidCLIEntryException("Setter for option ["
								+ annot.name()
								+ "] requires an array type: "
								+ methList[i], InvalidCLIEntryException.invalid_cause.bad_options);
					}
				}
			}
		}

		// validate any option sets
		CLIEntry clentry = obj.getClass().getAnnotation(CLIEntry.class);

		CLIOptionSet [] validSets = clentry.validarameterSets();
		CLIOptionSet [] invalidSets = clentry.invalidarameterSets();

		// validate that the options referenced exist
		validateOptionSet(validSets, instanceMap);
		validateOptionSet(invalidSets, instanceMap);

		// validate that no option names are duplicated
		validateSets(validSets);
		validateSets(invalidSets);

		// validate that the option sets are exclusive - cross both ways.  Always check if l is a subset of r
		validateExclusiveSets(validSets, invalidSets);
		validateExclusiveSets(invalidSets, validSets);

		// before handling, order the typeList appropriately
		typeList = orderOptions(clentry, typeList);

		for (Data data : typeList)
		{
				//here is an option - check it out
				Option option = new Option(
						data.clioption.name(),
						data.clioption.longName().equals(CLIOption.NULL_STRING_VALUE) ? null : data.clioption.longName(),
						data.clioption.valueType() != CLIOption.value_type.not_allowed,
						data.clioption.description()
				);

				option.setRequired(data.clioption.required());

				// in the case of arguments with values, get the value cardinalities worked out
				if (data.clioption.valueType() != CLIOption.value_type.not_allowed)
				{
					option.setArgs(1);

					if (data.clioption.valueType() == CLIOption.value_type.required)
					{
						option.setOptionalArg(false);
					}
					else if (data.clioption.valueType() == CLIOption.value_type.optional)
					{
						option.setOptionalArg(true);
					}
				}
				else
				{
					option.setArgs(0);
					option.setOptionalArg(false);
				}

				//option.setType();
				options.addOption(option);
		}

		// initially, check for the help option.  This must be done before apache gets to it.
		for (String option : argv)
		{
			// this is the signal to throw a usage error
			if (option.equals("-?") || option.equals("--help"))
			{
				if (argv.length != 1)
				{
					throw new UsageException(obj, typeList, HELP_INVALID_USAGE_MESSAGE);
				}

				throw new UsageException(obj, typeList, HELP_USAGE_MESSAGE);
			}
		}

		//options are created - pass the result to the command line
		CommandLineParser clp = null;

		switch (getParserType(obj))
		{
			case gnu:
				clp = new GnuParser();
				break;
			case posix:
				clp = new PosixParser();
				break;
			case basic:
				clp = new BasicParser();
				break;
		}

		try
		{
			CommandLine cl = clp.parse(options, argv);

			boolean vbose = false;

			if (cl.hasOption(help.name()) || cl.hasOption(help.longName()))
			{
				throw new UsageException(obj, typeList, HELP_INVALID_USAGE_MESSAGE);
			}

			if (cl.hasOption(verbose.name()) || cl.hasOption(verbose.longName()))
			{
				vbose = true;
			}

			// this map stores the actual options passed by the user for comparing with
			// the valid and invalid option sets
			Map optMap = new HashMap();

			// run through all options and determine values
			for (Data data : typeList)
			{
				boolean opted = cl.hasOption(data.clioption.name());

				// skip when there is no setter
				if (data.setter == null)
				{
					continue;
				}

				Object value;

				if (!opted)
				{
					if (data.clioption.defaultValue().equals("") && data.clioption.required())
					{
						throw new UsageException(obj, typeList, "Value not specified ["
								+ data.clioption.name()
								+ "] and no suitable default exists");
					}
				}
				else
				{
					optMap.put(data.clioption.name(), data);
				}

				try
				{
					value =
						resolveStringValue(data,
							cl,
							data.clioption.defaultValue().equals(CLIOption.NULL_STRING_VALUE) ? null : data.clioption.defaultValue());

					valueMap.put(data.clioption.name(), value);
					valueMap.put(data.clioption.longName(), value);
				}
				catch(UsageException exc)
				{
					// rethrow to populate missing values
					throw new UsageException(obj, typeList, exc);
				}
			}

			// before continuing, check all valid and invalid combinations
			checkValidSets(validSets, optMap, obj, typeList);
			checkInValidSets(invalidSets, optMap, obj, typeList);

			RecursiveMap rmap = new RecursiveMap(valueMap);

			// run through all options and pass the values on with injection
			for (Data data : typeList)
			{
				Object value = rmap.get(data.clioption.name());
				Object val = resolveOptionValue(data, cl, (String) value, obj, typeList);

				if (data.setter != null && val != null)
				{
					try
					{
						displayValueMap.put(StringUtils.rightPad(StringUtils.abbreviate(data.clioption.longName(), 30), 30), display(val));
						data.setter.invoke(obj, val);
					}
					catch (IllegalAccessException e)
					{
						throw new InvalidCLIEntryException("Could not access setter for option ["
								+ data.clioption.name()
								+ "]", e, InvalidCLIEntryException.invalid_cause.class_access_error);
					}
					catch (InvocationTargetException e)
					{
						throw new UsageException(obj, typeList, "Error setting option value [" + data.clioption.name() + "]", e);
					}
				}
			}

			if (vbose)
			{
				verboseOutput(obj, displayValueMap);
			}

			invokeMain(obj, argv);

			return obj;
		}
		catch (ParseException e)
		{
			throw new UsageException(obj, typeList, e);
		}
		catch (UsageException exc)
		{
			throw exc;
		}
		catch (Exception exc)
		{
			throw new RuntimeException(exc);
		}
	}

	/**
	 * Sort the Data array by the assignmentOrder of the entry.  Any unreferenced options default
	 * to current order (declaration order) and any names which aren't a option long or short name will
	 * cause an error.
	 * @param clentry
	 * @param typeList
	 * @return
	 */
	private static List orderOptions(CLIEntry clentry, List typeList) throws InvalidCLIEntryException {
		if (clentry.assignmentOrder().length == 0)
		{
			return typeList;
		}

		List orderedTypeList = new ArrayList();

		// store a reference to each option in the map by long and short name
		Map optionNameMap = new HashMap();

		for (Data data : typeList)
		{
			optionNameMap.put(data.clioption.name(), data);
			optionNameMap.put(data.clioption.longName(), data);
		}

		for (String option : clentry.assignmentOrder())
		{
			Data op = optionNameMap.get(option);

			if (op == null)
			{
				throw new InvalidCLIEntryException("Option '" + option + "' specified in assignmentOrder is not a declared option", InvalidCLIEntryException.invalid_cause.bad_option_assignment_order);
			}

			if (orderedTypeList.contains(op))
			{
				throw new InvalidCLIEntryException("Option '" + option + "' specified in assignmentOrder more than once", InvalidCLIEntryException.invalid_cause.bad_option_assignment_order);
			}

			orderedTypeList.add(op);
		}

		// add in all missing options in the order provided
		for (Data data : typeList)
		{
			if (!orderedTypeList.contains(data))
			{
				orderedTypeList.add(data);
			}
		}

		return orderedTypeList;
	}

	private static Object display(Object val) {
		if (val != null && val.getClass().isArray())
		{
			StringBuilder stb = new StringBuilder();

			Object [] obarr = getArray(val);

			for (Object obj : obarr)
			{
				stb.append(obj);
				stb.append(',');
			}

			if (stb.length() > 1)
			{
				stb.deleteCharAt(stb.length() - 1);
			}

			return stb.toString();
		}

		return String.valueOf(val);
	}

	private static final Class[] ARRAY_PRIMITIVE_TYPES = {
			int[].class, float[].class, double[].class, boolean[].class,
			byte[].class, short[].class, long[].class, char[].class };

	private static Object[] getArray(Object val){
		Class valKlass = val.getClass();
		Object[] outputArray = null;

		for(Class arrKlass : ARRAY_PRIMITIVE_TYPES){
			if(valKlass.isAssignableFrom(arrKlass)){
				int arrlength = Array.getLength(val);
				outputArray = new Object[arrlength];
				for(int i = 0; i < arrlength; ++i){
					outputArray[i] = Array.get(val, i);
				}
				break;
			}
		}
		if(outputArray == null) // not primitive type array
			outputArray = (Object[])val;

		return outputArray;
	}

	private static void checkInValidSets(CLIOptionSet[] invalidSets, Map optMap, Object obj, List typeList)
			throws UsageException
	{
		CLIOptionSet invalidSet = checkSet(invalidSets, optMap);

		if (invalidSet != null)
		{
			throw new UsageException(obj, typeList, "Invalid combination of parameters.  " + invalidSet.description());
		}
	}

	private static void checkValidSets(CLIOptionSet[] validSets, Map optMap, Object obj, List typeList)
			throws UsageException
	{
		if (validSets.length != 0 && checkSet(validSets, optMap) == null)
		{
			throw new UsageException(obj, typeList, "Invalid combination of parameters.  Does not match any valid option sets.");
		}
	}

	private static CLIOptionSet checkSet(CLIOptionSet[] validSets, Map optMap)
	{
		if (validSets.length != 0)
		{
			for (CLIOptionSet set : validSets)
			{
				if (matches(set, optMap))
				{
					return set;
				}
			}
		}

		return null;
	}

	private static boolean matches(CLIOptionSet set, Map optMap)
	{
		String [] names = set.optionShortNames();

		CLIOptionSet.set_type type = set.setType();

		switch (type)
		{
			case subset_of_args:
				// if the set is larger than the options, it can't be a subset
				if (names.length > optMap.size())
				{
					return false;
				}
				break;

			case exact_match:
				// if both sides aren't the same length, then they can't match
				if (names.length != optMap.size())
				{
					return false;
				}

				break;
		}

		// now that lengths have been validated, check that set is at least a subset of optMap.  This wil satisfy both conditions
		for (String name : names)
		{
			if (!optMap.containsKey(name))
			{
				return false;
			}
		}

		return true;
	}

	private static void validateSets(CLIOptionSet[] invalidSets) throws InvalidCLIEntryException
	{
		for (CLIOptionSet set : invalidSets)
		{
			String [] snames = set.optionShortNames();

			for (String lsname : snames)
			{
				// for each option name, count the number of occurrences.  Each must be exactly 1
				int occCount = 0;

				for (String rsname : snames)
				{
					if (lsname.equals(rsname))
					{
						occCount++;
					}
				}

				if (occCount != 1)
				{
					throw new InvalidCLIEntryException("Option duplciated in set '" + lsname + "'", InvalidCLIEntryException.invalid_cause.bad_option_set);
				}
			}
		}
	}

	private static void validateExclusiveSets(CLIOptionSet[] validSets, CLIOptionSet[] invalidSets)
			throws InvalidCLIEntryException
	{
		for (CLIOptionSet lset : validSets)
		{
			String [] lnames = lset.optionShortNames();

			for (CLIOptionSet rset : invalidSets)
			{
				String [] rnames = rset.optionShortNames();

				// compare the two sets
				compareExclusive(lnames, lset.id(), lset.setType(), rnames, rset.id(), rset.setType());
			}
		}
	}

	private static void compareExclusive(String [] lnames, String lid, CLIOptionSet.set_type lset_type, String [] rnames, String rid, CLIOptionSet.set_type rset_type) throws InvalidCLIEntryException
	{
		int overlapCount = 0;

		for (String lname : lnames)
		{
			boolean found = false;

			for (String rname : rnames)
			{
				if (lname.equals(rname))
				{
					// break - no need to continue
					found = true;
					break;
				}
			}

			if (found)
			{
				overlapCount++;
			}
		}

		if (
			(lnames.length == rnames.length)
			&&
			(overlapCount == lnames.length))
		{
			//sets are lnames is a subset or rnames.  Fail loudly
			throw new InvalidCLIEntryException("Option sets overlap. [" + lid + ":" + rid + "]", InvalidCLIEntryException.invalid_cause.bad_option_set);
		}

		//TODO - handle subsets
	}

	private static void validateOptionSet(CLIOptionSet[] validSets, Map instanceMap)
			throws InvalidCLIEntryException
	{
		for (CLIOptionSet set : validSets)
		{
			String[] shnames = set.optionShortNames();

			for (String shname : shnames)
			{
				if (!instanceMap.containsKey(shname))
				{
					throw new InvalidCLIEntryException("Option '" + shname + "' from set '" + set.description() + "' not found in the command object", InvalidCLIEntryException.invalid_cause.bad_option_set);
				}
			}
		}
	}

	private static void assertValid(CLIOption annot) throws InvalidCLIEntryException
	{
		if (annot.required() && !annot.defaultValue().equals(CLIOption.NULL_STRING_VALUE))
		{
			throw new InvalidCLIEntryException(
				"Required options cannot have default values: " + annot.name(),
				InvalidCLIEntryException.invalid_cause.bad_options
			);
		}

		// flags can only have a default value of 'true', 'false'
		if (annot.valueType() == CLIOption.value_type.not_allowed)
		{
			if (!annot.defaultValue().equals(CLIOption.NULL_STRING_VALUE) && !annot.defaultValue().equals("true") && !annot.defaultValue().equals("false"))
			{
				throw new InvalidCLIEntryException(
						"Flags cannot have default values: " + annot.name(),
						InvalidCLIEntryException.invalid_cause.bad_options
				);
			}
		}
	}

	private static void verboseOutput(Object ent, Map valueMap)
	{
		CLIEntry entr = ent.getClass().getAnnotation(CLIEntry.class);

		Map context = new HashMap();

		context.put("options", valueMap.entrySet());
		context.put("entry", entr);
		context.put("datetime", new Timestamp(System.currentTimeMillis()));

		String nick = entr.nickName();

		if (nick.equals(CLIOption.NULL_STRING_VALUE))
		{
			nick = entr.getClass().getSimpleName();
		}

		context.put("nickName", nick);

		try
		{
			String template = IOUtils.toString(ent.getClass().getResource("/runtime_options.vm"));

			VelocityContext vcontext = new VelocityContext(context);

			VelocityEngine velocityEngine = new VelocityEngine();

			velocityEngine.init();

			StringWriter w = new StringWriter();

			velocityEngine.evaluate(vcontext, w, "log", template);

			System.out.println(w.toString());
		}
		catch (IOException e)
		{
			throw new RuntimeException(e);
		}
	}

	private static String print(Object value)
	{
		return String.valueOf(value);
	}

	private static void invokeMain(Object obj, String[] argv) throws InvocationTargetException, IllegalAccessException
	{
		Method [] methods = obj.getClass().getMethods();

		for (Method method : methods)
		{
			CLIMain climain = method.getAnnotation(CLIMain.class);

			if (climain != null)
			{
				// this is the one.
				if (method.getParameterTypes().length == 0)
				{
					method.invoke(obj);
				}
				else
				{
					// this one needs the string parameters
					method.invoke(obj, new Object [] {argv});
				}
			}
		}
	}

	private static CLIEntry.parser_type getParserType(Object obj)
	{
		CLIEntry clentry = obj.getClass().getAnnotation(CLIEntry.class);

		return clentry.parserType();
	}

	private static Object resolveStringValue(Data data, CommandLine cl, String def) throws UsageException
	{
		boolean hasOption = cl.hasOption(data.clioption.name());
		String textValue = hasOption ? cl.getOptionValue(data.clioption.name()) : def;

		if (data.cardinality == cardinal_type.array)
		{
		}
		else if (data.cardinality == cardinal_type.instance)
		{
			if (!hasOption)
			{
				textValue = def;
			}
		}
		else if (data.cardinality == cardinal_type.flag)
		{
			if (!hasOption)
			{
				textValue = def;
			}
		}

		return textValue;
	}

	private static Object resolveOptionValue(Data data, CommandLine cl, String textValue, Object cliEntry, List options) throws UsageException
	{
		Object value = null;

		// we already validated earlier that there is exactly one parameter
		if (data.cardinality != cardinal_type.flag)
		{
			if (textValue == null)
			{
				return null;
			}

			if (data.cardinality == cardinal_type.array)
			{
				String [] optionValue = getArrayValue(textValue, data.clioption.valueSeparator());

				// validate against enumerations if present
				validateEnum(data, value, cliEntry, options);

				if (data.clioption.valueCardinality() != CLIOption.UNLIMITED_VALUES)
				{
					if (optionValue.length != data.clioption.valueCardinality())
					{
						throw new UsageException(cliEntry, options, "Wrong number of values supplied for option [" + data.clioption
								.name() + "]: " + Arrays.asList(optionValue));
					}
				}

				Object[] valueArr = optionValue;
				boolean[] bValueArr = null;
				int[] iValueArr = null;
				long[] lValueArr = null;

				if (data.type == data_type.bool)
				{
					bValueArr = new boolean[optionValue.length];
				}
				else if (data.type == data_type.Bool)
				{
					valueArr = new Boolean[optionValue.length];
				}
				else if (data.type == data_type.integer)
				{
					iValueArr = new int[optionValue.length];
				}
				else if (data.type == data_type.Integer)
				{
					valueArr = new Integer[optionValue.length];
				}
				else if (data.type == data_type.long_integer)
				{
					lValueArr = new long[optionValue.length];
				}
				else if (data.type == data_type.Long)
				{
					valueArr = new Long[optionValue.length];
				}
				else if (data.type == data_type.String)
				{
					valueArr = new String[optionValue.length];
				}

				for (int optIndex = 0; optIndex < optionValue.length; optIndex++)
				{
					if (data.type == data_type.Bool)
					{
						valueArr[optIndex] = Boolean.valueOf(optionValue[optIndex]);
					}
					else if (data.type == data_type.bool)
					{
						bValueArr[optIndex] = Boolean.valueOf(optionValue[optIndex]);
					}
					else if (data.type == data_type.integer)
					{
						iValueArr[optIndex] = Integer.parseInt(optionValue[optIndex]);
					}
					else if (data.type == data_type.Integer)
					{
						valueArr[optIndex] = Integer.parseInt(optionValue[optIndex]);
					}
					else if (data.type == data_type.long_integer)
					{
						lValueArr[optIndex] = Long.parseLong(optionValue[optIndex]);
					}
					else if (data.type == data_type.Long)
					{
						valueArr[optIndex] = Long.parseLong(optionValue[optIndex]);
					}
					else if (data.type == data_type.String || data.type == data_type.Object)
					{
						valueArr[optIndex] = optionValue[optIndex];
					}
				}

				if (bValueArr != null)
				{
					value = bValueArr;
				}
				else if (iValueArr != null)
				{
					value = iValueArr;
				}
				else
				{
					value = valueArr;
				}
			}
			else
			{
				value = textValue;

				validateEnum(data, value, cliEntry, options);

				if (data.type == data_type.Bool || data.type == data_type.bool)
				{
					value = Boolean.valueOf(textValue);
				}
				else if (data.type == data_type.integer || data.type == data_type.Integer)
				{
					value = Integer.parseInt(textValue);
				}
				else if (data.type == data_type.long_integer || data.type == data_type.Long)
				{
					value = Long.parseLong(textValue);
				}
			}
		}
		else
		{
			//  determine the correct action here.
			// 1 - if there is no value supplied, chose a default based on whether the option was supplied.
			// boolean types use false, integral types get 0
			if (textValue == null)
			{
				boolean hasOption = cl.hasOption(data.clioption.name());

				switch (data.type)
				{
					case bool:
					case Bool:
						value = hasOption;
						break;
					case long_integer:
					case Long:
					case integer:
					case Integer:
						value = hasOption ? 1 : 0;
						break;
					case string:
					case String:
					case Object:
						value = hasOption ? "true" : "false";
						break;
				}
			}
			else
			{
				value = false;

				// try several methods for interpretation
				if (textValue.equals("1"))
				{
					value = true;
				}
				else if (textValue.equals("0"))
				{
					value = false;
				}
				else if (textValue.equals("true"))
				{
					value = true;
				}
				else if (textValue.equals("false"))
				{
					value = false;
				}
			}
		}

		return value;
	}

	private static void validateEnum(Data data, Object value, Object cliEntry, List options) throws UsageException
	{
		String[] atr = data.clioption.enumeratedValues();

		if (atr.length != 0)
		{
			boolean match = false;

			for (String enu : atr)
			{
				if (enu.equals(value))
				{
					match = true;
					break;
				}
			}

			if (!match)
			{
				throw new UsageException(cliEntry, options, "Option [" + data.clioption.name() + "] value [" + value + "] does not match enumeration");
			}
		}
	}

	public static String [] getArrayValue(String textValue, char c)
	{
		// split on every value, ignoring escaped separators denoted by ESCAPE_CHARACTER_DEFAULT
		// pass over the input two times, once looking for \() constructs, and the second
		// time resolving escape sequences
		StringBuilder builder = new StringBuilder();

		EscapeFunctionExpression escapeFunctionExpression = new EscapeFunctionExpression(textValue);

		int lastEnd = 0;

		while (escapeFunctionExpression.hasNext())
		{
			String escText = escapeFunctionExpression.getEscapeFunction();

			// check the preChar - an even number means pass it through.
			// in the case of an odd number, remove the last one and escape the sequence

			String pre = escapeFunctionExpression.getPreChar();

			if (pre.length() % 2 == 0)
			{
				// pass all through unmodified
				builder.append(textValue.substring(lastEnd, escapeFunctionExpression.start()));
				builder.append(escapeFunctionExpression.group(0));
			}
			else
			{
				if (pre.length() > 1)
				{
					// remove one char, for our escape
					builder.append(pre.substring(1));
				}

				builder.append(textValue.substring(lastEnd, escapeFunctionExpression.start()));

				builder.append(escapeText(escText, c));
			}

			lastEnd = escapeFunctionExpression.end();
		}

		if (lastEnd < textValue.length())
		{
			builder.append(textValue.substring(lastEnd));
		}

		// scan and process escape sequences
		String bu = builder.toString();

		List strings = new ArrayList();

		builder.setLength(0);

		boolean lastCharWasEscape = false;

		for (char ch : bu.toCharArray())
		{
			if (ch == CLIOption.ESCAPE_CHARACTER_DEFAULT)
			{
				if (lastCharWasEscape)
				{
					// append a single escape char
					builder.append(CLIOption.ESCAPE_CHARACTER_DEFAULT);
					lastCharWasEscape = false;
				}
				else
				{
					lastCharWasEscape = true;
				}
			}
			else if (ch == c)
			{
				if (lastCharWasEscape)
				{
					// pass on the character
					builder.append(ch);
				}
				else
				{
					strings.add(builder.toString().replace("@-", "-"));
					builder.setLength(0);
				}

				lastCharWasEscape = false;
			}
			else
			{
				// pass the char on
				if (lastCharWasEscape)
				{
					throw new IllegalArgumentException("Illegal escaped character: " + ch);
				}

				builder.append(ch);

				lastCharWasEscape = false;
			}
		}

		if (lastCharWasEscape)
		{
			throw new IllegalArgumentException("unterminated escaped character");
		}

		if (builder.length() > 0)
		{
			// do a quick replace of '@-' with '-'
			strings.add(builder.toString().replace("@-", "-"));
		}

		return strings.toArray(new String[strings.size()]);
	}

	// Escape occurrences of the separator char OR the escape char
	private static String escapeText(String substring, char c) {
		// replace every occurrence of the separator char (c) with an escape sequence
		return substring.replace("\\", "\\\\").replaceAll(Pattern.quote(String.valueOf(c)), "\\\\$0");
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy