Use local copy of latest bndlib code for pre-release testing purposes

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1347815 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/lib/getopt/CommandLine.java b/bundleplugin/src/main/java/aQute/lib/getopt/CommandLine.java
new file mode 100644
index 0000000..f95b6c4
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/getopt/CommandLine.java
@@ -0,0 +1,472 @@
+package aQute.lib.getopt;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.*;
+
+import aQute.configurable.*;
+import aQute.lib.justif.*;
+import aQute.libg.generics.*;
+import aQute.libg.reporter.*;
+
+/**
+ * Helps parsing command lines. This class takes target object, a primary
+ * command, and a list of arguments. It will then find the command in the target
+ * object. The method of this command must start with a "_" and take an
+ * parameter of Options type. Usually this is an interface that extends Options.
+ * The methods on this interface are options or flags (when they return
+ * boolean).
+ * 
+ */
+@SuppressWarnings("unchecked") public class CommandLine {
+	static int		LINELENGTH	= 60;
+	static Pattern	ASSIGNMENT	= Pattern.compile("(\\w[\\w\\d]*+)\\s*=\\s*([^\\s]+)\\s*");
+	Reporter		reporter;
+	Justif			justif = new Justif(60);
+	
+	public CommandLine(Reporter reporter) {
+		this.reporter = reporter;
+	}
+
+	/**
+	 * Execute a command in a target object with a set of options and arguments
+	 * and returns help text if something fails. Errors are reported.
+	 */
+
+	public String execute(Object target, String cmd, List<String> input) throws Exception {
+
+		if (cmd.equals("help")) {
+			StringBuilder sb = new StringBuilder();
+			Formatter f = new Formatter(sb);
+			if (input.isEmpty())
+				help(f, target);
+			else {
+				for (String s : input) {
+					help(f, target, s);
+				}
+			}
+			f.flush();
+			justif.wrap(sb);
+			return sb.toString();
+		}
+
+		//
+		// Find the appropriate method
+		//
+
+		List<String> arguments = new ArrayList<String>(input);
+		Map<String, Method> commands = getCommands(target);
+
+		Method m = commands.get(cmd);
+		if (m == null) {
+			reporter.error("No such command %s\n", cmd);
+			return help(target, null, null);
+		}
+
+		//
+		// Parse the options
+		//
+
+		Class<? extends Options> optionClass = (Class<? extends Options>) m.getParameterTypes()[0];
+		Options options = getOptions(optionClass, arguments);
+		if (options == null) {
+			// had some error, already reported
+			return help(target, cmd, null);
+		}
+
+		// Check if we have an @Arguments annotation that
+		// provides patterns for the remainder arguments
+
+		Arguments argumentsAnnotation = optionClass.getAnnotation(Arguments.class);
+		if (argumentsAnnotation != null) {
+			String[] patterns = argumentsAnnotation.arg();
+
+			// Check for commands without any arguments
+
+			if (patterns.length == 0 && arguments.size() > 0) {
+				reporter.error("This command takes no arguments but found %s\n", arguments);
+				return help(target, cmd, null);
+			}
+
+			// Match the patterns to the given command line
+
+			int i = 0;
+			for (; i < patterns.length; i++) {
+				String pattern = patterns[i];
+
+				boolean optional = pattern.matches("\\[.*\\]");
+
+				// Handle vararg
+
+				if (pattern.equals("...")) {
+					i = Integer.MAX_VALUE;
+					break;
+				}
+
+				// Check if we're running out of args
+
+				if (i > arguments.size()) {
+					if (!optional)
+						reporter.error("Missing argument %s\n", patterns[i]);
+					return help(target, cmd, optionClass);
+				}
+			}
+
+			// Check if we have unconsumed arguments left
+
+			if (i < arguments.size()) {
+				reporter.error("Too many arguments specified %s, expecting %s\n", arguments,
+						Arrays.asList(patterns));
+				return help(target, cmd, optionClass);
+			}
+		}
+		if (reporter.getErrors().size() == 0) {
+			m.setAccessible(true);
+			m.invoke(target, options);
+			return null;
+		}
+		return help(target, cmd, optionClass);
+	}
+
+	private String help(Object target, String cmd, Class<? extends Options> type) throws Exception {
+		StringBuilder sb = new StringBuilder();
+		Formatter f = new Formatter(sb);
+		if (cmd == null)
+			help(f, target);
+		else if (type == null)
+			help(f, target, cmd);
+		else
+			help(f, target, cmd, type);
+
+		f.flush();
+		justif.wrap(sb);
+		return sb.toString();
+	}
+
+	/**
+	 * Parse the options in a command line and return an interface that provides
+	 * the options from this command line. This will parse up to (and including)
+	 * -- or an argument that does not start with -
+	 * 
+	 */
+	public <T extends Options> T getOptions(Class<T> specification, List<String> arguments)
+			throws Exception {
+		Map<String, String> properties = Create.map();
+		Map<String, Object> values = new HashMap<String, Object>();
+		Map<String, Method> options = getOptions(specification);
+
+		argloop: while (arguments.size() > 0) {
+
+			String option = arguments.get(0);
+
+			if (option.startsWith("-")) {
+
+				arguments.remove(0);
+
+				if (option.startsWith("--")) {
+
+					if ("--".equals(option))
+						break argloop;
+
+					// Full named option, e.g. --output
+					String name = option.substring(2);
+					Method m = options.get(name);
+					if (m == null)
+						reporter.error("Unrecognized option %s\n", name);
+					else
+						assignOptionValue(values, m, arguments, true);
+
+				} else {
+
+					// Set of single character named options like -a
+
+					charloop: for (int j = 1; j < option.length(); j++) {
+
+						char optionChar = option.charAt(j);
+
+						for (Entry<String, Method> entry : options.entrySet()) {
+							if (entry.getKey().charAt(0) == optionChar) {
+								boolean last = (j + 1) >= option.length();
+								assignOptionValue(values, entry.getValue(),
+										arguments, last);
+								continue charloop;
+							}
+						}
+						reporter.error("No such option -%s\n", optionChar);
+					}
+				}
+			} else {
+				Matcher m = ASSIGNMENT.matcher(option);
+				if (m.matches()) {
+					properties.put(m.group(1), m.group(2));
+				}
+				break;
+			}
+		}
+
+		// check if all required elements are set
+
+		for (Entry<String, Method> entry : options.entrySet()) {
+			Method m = entry.getValue();
+			String name = entry.getKey();
+			if (!values.containsKey(name) && isMandatory(m))
+				reporter.error("Required option --%s not set", name);
+		}
+
+		values.put(".", arguments);
+		values.put(".command", this);
+		values.put(".properties", properties);
+		return Configurable.createConfigurable(specification, values);
+	}
+
+	/**
+	 * Answer a list of the options specified in an options interface
+	 */
+	private Map<String, Method> getOptions(Class<? extends Options> interf) {
+		Map<String, Method> map = new TreeMap<String, Method>();
+
+		for (Method m : interf.getMethods()) {
+			if (m.getName().startsWith("_"))
+				continue;
+
+			String name;
+
+			Config cfg = m.getAnnotation(Config.class);
+			if (cfg == null || cfg.id() == null || cfg.id().equals(Config.NULL))
+				name = m.getName();
+			else
+				name = cfg.id();
+
+			map.put(name, m);
+		}
+		return map;
+	}
+
+	/**
+	 * Assign an option, must handle flags, parameters, and parameters that can
+	 * happen multiple times.
+	 * 
+	 * @param options
+	 *            The command line map
+	 * @param args
+	 *            the args input
+	 * @param i
+	 *            where we are
+	 * @param m
+	 *            the selected method for this option
+	 * @param last
+	 *            if this is the last in a multi single character option
+	 * @return
+	 */
+	public void assignOptionValue(Map<String, Object> options, Method m, List<String> args,
+			boolean last) {
+		String name = m.getName();
+		Type type = m.getGenericReturnType();
+
+		if (isOption(m)) {
+
+			// The option is a simple flag
+
+			options.put(name, true);
+		} else {
+
+			// The option is followed by an argument
+
+			if (!last) {
+				reporter.error(
+						"Option --%s not last in a set of 1-letter options (%s) but it requires an argument of type ",
+						name, name.charAt(0), getTypeDescriptor(type));
+				return;
+			}
+
+			if (args.isEmpty()) {
+				reporter.error("Missing argument %s for option --%s, -%s ",
+						getTypeDescriptor(type), name, name.charAt(0));
+				return;
+			}
+
+			String parameter = args.remove(0);
+
+			if (Collection.class.isAssignableFrom(m.getReturnType())) {
+
+				Collection<Object> optionValues = (Collection<Object>) options.get(m.getName());
+
+				if (optionValues == null) {
+					optionValues = new ArrayList<Object>();
+					options.put(name, optionValues);
+				}
+
+				optionValues.add(parameter);
+			} else {
+
+				if (options.containsKey(name)) {
+					reporter.error("The option %s can only occur once", name);
+					return;
+				}
+
+				options.put(name, parameter);
+			}
+		}
+	}
+
+	/**
+	 * Provide a help text.
+	 */
+
+	public void help(Formatter f, Object target, String cmd, Class<? extends Options> specification) {
+		Description descr = specification.getAnnotation(Description.class);
+		Arguments patterns = specification.getAnnotation(Arguments.class);
+		Map<String, Method> options = getOptions(specification);
+
+		String description = descr == null ? "" : descr.value();
+
+		f.format("NAME\n  %s - %s\n\n", cmd, description);
+		f.format("SYNOPSIS\n   %s [options] ", cmd);
+
+		if (patterns == null)
+			f.format(" ...\n\n");
+		else {
+			String del = " ";
+			for (String pattern : patterns.arg()) {
+				if (pattern.equals("..."))
+					f.format("%s...", del);
+				else
+					f.format("%s<%s>", del, pattern);
+				del = " ";
+			}
+			f.format("\n\n");
+		}
+
+		f.format("OPTIONS\n");
+		for (Entry<String, Method> entry : options.entrySet()) {
+			String optionName = entry.getKey();
+			Method m = entry.getValue();
+
+			Config cfg = m.getAnnotation(Config.class);
+			Description d = m.getAnnotation(Description.class);
+			boolean required = isMandatory(m);
+
+			String methodDescription = cfg != null ? cfg.description() : (d == null ? "" : d
+					.value());
+
+			f.format("   %s -%s, --%s %s%s - %s\n", required ? " " : "[", //
+					optionName.charAt(0), //
+					optionName, //
+					getTypeDescriptor(m.getGenericReturnType()), //
+					required ? " " : "]",//
+					methodDescription);
+		}
+		f.format("\n");
+	}
+
+	static Pattern	LAST_PART	= Pattern.compile(".*[\\$\\.]([^\\$\\.]+)");
+
+	private static String lastPart(String name) {
+		Matcher m = LAST_PART.matcher(name);
+		if (m.matches())
+			return m.group(1);
+		return name;
+	}
+
+	/**
+	 * Show all commands in a target
+	 */
+	public void help(Formatter f, Object target) throws Exception {
+		// TODO get help from the class
+		Description descr = target.getClass().getAnnotation(Description.class);
+		if (descr != null) {
+			f.format("%s\n\n", descr.value());
+		}
+		f.format("Available commands: ");
+
+		String del = "";
+		for (String name : getCommands(target).keySet()) {
+			f.format("%s%s", del, name);
+			del = ", ";
+		}
+		f.format("\n");
+
+	}
+
+	/**
+	 * Show the full help for a given command
+	 */
+	public void help(Formatter f, Object target, String cmd) {
+
+		Method m = getCommands(target).get(cmd);
+		if (m == null)
+			f.format("No such command: %s\n", cmd);
+		else {
+			Class<? extends Options> options = (Class<? extends Options>) m.getParameterTypes()[0];
+			help(f, target, cmd, options);
+		}
+	}
+
+	/**
+	 * Parse a class and return a list of command names
+	 * 
+	 * @param target
+	 * @return
+	 */
+	public Map<String, Method> getCommands(Object target) {
+		Map<String, Method> map = new TreeMap<String, Method>();
+
+		for (Method m : target.getClass().getMethods()) {
+
+			if (m.getParameterTypes().length == 1 && m.getName().startsWith("_")) {
+				Class<?> clazz = m.getParameterTypes()[0];
+				if (Options.class.isAssignableFrom(clazz)) {
+					String name = m.getName().substring(1);
+					map.put(name, m);
+				}
+			}
+		}
+		return map;
+	}
+
+	/**
+	 * Answer if the method is marked mandatory
+	 */
+	private boolean isMandatory(Method m) {
+		Config cfg = m.getAnnotation(Config.class);
+		if (cfg == null)
+			return false;
+
+		return cfg.required();
+	}
+
+	/**
+	 * @param m
+	 * @return
+	 */
+	private boolean isOption(Method m) {
+		return m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class;
+	}
+
+	/**
+	 * Show a type in a nice way
+	 */
+
+	private String getTypeDescriptor(Type type) {
+		if (type instanceof ParameterizedType) {
+			ParameterizedType pt = (ParameterizedType) type;
+			Type c = pt.getRawType();
+			if (c instanceof Class) {
+				if (Collection.class.isAssignableFrom((Class<?>) c)) {
+					return getTypeDescriptor(pt.getActualTypeArguments()[0]) + "*";
+				}
+			}
+		}
+		if (!(type instanceof Class))
+			return "<>";
+
+		Class<?> clazz = (Class<?>) type;
+
+		if (clazz == Boolean.class || clazz == boolean.class)
+			return ""; // Is a flag
+
+		return "<" + lastPart(clazz.getName().toLowerCase()) + ">";
+	}
+
+}