/*
* Copyright (c) 2005, Sam Pullara. All Rights Reserved.
* You may modify and redistribute as long as this attribution remains.
*/
package com.sampullara.cli;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.PrintStream;
import java.lang.reflect.*;
import java.util.*;
public class Args {
/**
* A convenience method for parsing and automatically producing error messages.
*
* @param target Either an instance or a class
* @param args The arguments you want to parse and populate
* @return The list of arguments that were not consumed
*/
public static List<String> parseOrExit(Object target, String[] args) {
try {
return parse(target, args);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
Args.usage(target);
System.exit(1);
throw e;
}
}
/**
* Parse a set of arguments and populate the target with the appropriate values.
*
* @param target Either an instance or a class
* @param args The arguments you want to parse and populate
* @return The list of arguments that were not consumed
*/
public static List<String> parse(Object target, String[] args) {
List<String> arguments = new ArrayList<String>();
arguments.addAll(Arrays.asList(args));
Class<?> clazz;
if (target instanceof Class) {
clazz = (Class) target;
} else {
clazz = target.getClass();
try {
BeanInfo info = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
processProperty(target, pd, arguments);
}
} catch (IntrospectionException e) {
// If its not a JavaBean we ignore it
}
}
// Check fields of 'target' class and its superclasses
for (Class<?> currentClazz = clazz; currentClazz != null; currentClazz = currentClazz.getSuperclass()) {
for (Field field : currentClazz.getDeclaredFields()) {
processField(target, field, arguments);
}
}
for (String argument : arguments) {
if (argument.startsWith("-")) {
throw new IllegalArgumentException("Invalid argument: " + argument);
}
}
return arguments;
}
private static void processField(Object target, Field field, List<String> arguments) {
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
boolean set = false;
for (Iterator<String> i = arguments.iterator(); i.hasNext();) {
String arg = i.next();
String prefix = argument.prefix();
String delimiter = argument.delimiter();
if (arg.startsWith(prefix)) {
Object value;
String name = getName(argument, field);
String alias = getAlias(argument);
arg = arg.substring(prefix.length());
Class<?> type = field.getType();
if (arg.equals(name) || (alias != null && arg.equals(alias))) {
i.remove();
value = consumeArgumentValue(type, argument, i);
if (!set) {
setField(type, field, target, value, delimiter);
} else {
addArgument(type, field, target, value, delimiter);
}
set = true;
}
if (set && !type.isArray()) break;
}
}
if (!set && argument.required()) {
String name = getName(argument, field);
throw new IllegalArgumentException("You must set argument " + name);
}
}
}
private static void addArgument(Class type, Field field, Object target, Object value, String delimiter) {
try {
Object[] os = (Object[]) field.get(target);
Object[] vs = (Object[]) getValue(type, value, delimiter);
Object[] s = (Object[]) Array.newInstance(type.getComponentType(), os.length + vs.length);
System.arraycopy(os, 0, s, 0, os.length);
System.arraycopy(vs, 0, s, os.length, vs.length);
field.set(target, s);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set field " + field, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
}
}
private static void addPropertyArgument(Class type, PropertyDescriptor property, Object target, Object value, String delimiter) {
try {
Object[] os = (Object[]) property.getReadMethod().invoke(target);
Object[] vs = (Object[]) getValue(type, value, delimiter);
Object[] s = (Object[]) Array.newInstance(type.getComponentType(), os.length + vs.length);
System.arraycopy(os, 0, s, 0, os.length);
System.arraycopy(vs, 0, s, os.length, vs.length);
property.getWriteMethod().invoke(target, (Object) s);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set property " + property, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Failed to validate argument " + value + " for " + property);
}
}
private static void processProperty(Object target, PropertyDescriptor property, List<String> arguments) {
Method writeMethod = property.getWriteMethod();
if (writeMethod != null) {
Argument argument = writeMethod.getAnnotation(Argument.class);
if (argument != null) {
boolean set = false;
for (Iterator<String> i = arguments.iterator(); i.hasNext();) {
String arg = i.next();
String prefix = argument.prefix();
String delimiter = argument.delimiter();
if (arg.startsWith(prefix)) {
Object value;
String name = getName(argument, property);
String alias = getAlias(argument);
arg = arg.substring(prefix.length());
Class<?> type = property.getPropertyType();
if (arg.equals(name) || (alias != null && arg.equals(alias))) {
i.remove();
value = consumeArgumentValue(type, argument, i);
if (!set) {
setProperty(type, property, target, value, delimiter);
} else {
addPropertyArgument(type, property, target, value, delimiter);
}
set = true;
}
if (set && !type.isArray()) break;
}
}
if (!set && argument.required()) {
String name = getName(argument, property);
throw new IllegalArgumentException("You must set argument " + name);
}
}
}
}
/**
* Generate usage information based on the target annotations.
*
* @param target An instance or class.
*/
public static void usage(Object target) {
usage(System.err, target);
}
public static void usage(Object target, String syntax) {
usage(System.err, target, syntax);
}
/**
* Generate usage information based on the target annotations.
*
* @param errStream A {@link java.io.PrintStream} to print the usage information to.
* @param target An instance or class.
*/
public static void usage(PrintStream errStream, Object target, String syntax) {
Class<?> clazz;
if (target instanceof Class) {
clazz = (Class) target;
} else {
clazz = target.getClass();
}
errStream.println("Usage: " + syntax);
for (Class<?> currentClazz = clazz; currentClazz != null; currentClazz = currentClazz.getSuperclass()) {
for (Field field : currentClazz.getDeclaredFields()) {
fieldUsage(errStream, target, field);
}
}
try {
BeanInfo info = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
propertyUsage(errStream, target, pd);
}
} catch (IntrospectionException e) {
// If its not a JavaBean we ignore it
}
}
/**
* Generate usage information based on the target annotations.
*
* @param errStream A {@link java.io.PrintStream} to print the usage information to.
* @param target An instance or class.
*/
public static void usage(PrintStream errStream, Object target) {
Class<?> clazz;
if (target instanceof Class) {
clazz = (Class) target;
} else {
clazz = target.getClass();
}
errStream.println("Usage: " + clazz.getName());
for (Class<?> currentClazz = clazz; currentClazz != null; currentClazz = currentClazz.getSuperclass()) {
for (Field field : currentClazz.getDeclaredFields()) {
fieldUsage(errStream, target, field);
}
}
try {
BeanInfo info = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
propertyUsage(errStream, target, pd);
}
} catch (IntrospectionException e) {
// If its not a JavaBean we ignore it
}
}
private static void fieldUsage(PrintStream errStream, Object target, Field field) {
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
String name = getName(argument, field);
String alias = getAlias(argument);
String prefix = argument.prefix();
String delimiter = argument.delimiter();
String description = argument.description();
makeAccessible(field);
try {
Object defaultValue = field.get(target);
Class<?> type = field.getType();
propertyUsage(errStream, prefix, name, alias, type, delimiter, description, defaultValue);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Could not use thie field " + field + " as an argument field", e);
}
}
}
private static void propertyUsage(PrintStream errStream, Object target, PropertyDescriptor field) {
Method writeMethod = field.getWriteMethod();
if (writeMethod != null) {
Argument argument = writeMethod.getAnnotation(Argument.class);
if (argument != null) {
String name = getName(argument, field);
String alias = getAlias(argument);
String prefix = argument.prefix();
String delimiter = argument.delimiter();
String description = argument.description();
try {
Method readMethod = field.getReadMethod();
Object defaultValue;
if (readMethod == null) {
defaultValue = null;
} else {
defaultValue = readMethod.invoke(target, (Object[]) null);
}
Class<?> type = field.getPropertyType();
propertyUsage(errStream, prefix, name, alias, type, delimiter, description, defaultValue);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Could not use thie field " + field + " as an argument field", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Could not get default value for " + field, e);
}
}
}
}
private static void propertyUsage(PrintStream errStream, String prefix, String name, String alias, Class<?> type, String delimiter, String description, Object defaultValue) {
StringBuilder sb = new StringBuilder(" ");
sb.append(prefix);
sb.append(name);
if (alias != null) {
sb.append(" (");
sb.append(prefix);
sb.append(alias);
sb.append(")");
}
if (type == Boolean.TYPE || type == Boolean.class) {
sb.append(" [flag] ");
sb.append(description);
} else {
sb.append(" [");
if (type.isArray()) {
String typeName = getTypeName(type.getComponentType());
sb.append(typeName);
sb.append("[");
sb.append(delimiter);
sb.append("]");
} else {
String typeName = getTypeName(type);
sb.append(typeName);
}
sb.append("] ");
sb.append(description);
if (defaultValue != null) {
sb.append(" (");
if (type.isArray()) {
List<Object> list = new ArrayList<Object>();
int len = Array.getLength(defaultValue);
for (int i = 0; i < len; i++) {
list.add(Array.get(defaultValue, i));
}
sb.append(list);
} else {
sb.append(defaultValue);
}
sb.append(")");
}
}
errStream.println(sb);
}
private static String getTypeName(Class<?> type) {
String typeName = type.getName();
int beginIndex = typeName.lastIndexOf(".");
typeName = typeName.substring(beginIndex + 1);
return typeName;
}
static String getName(Argument argument, PropertyDescriptor property) {
String name = argument.value();
if (name.equals("")) {
name = property.getName();
}
return name;
}
private static Object consumeArgumentValue(Class<?> type, Argument argument, Iterator<String> i) {
Object value;
if (type == Boolean.TYPE || type == Boolean.class) {
value = true;
} else {
if (i.hasNext()) {
value = i.next();
i.remove();
} else {
throw new IllegalArgumentException("Must have a value for non-boolean argument " + argument.value());
}
}
return value;
}
static void setProperty(Class<?> type, PropertyDescriptor property, Object target, Object value, String delimiter) {
try {
value = getValue(type, value, delimiter);
property.getWriteMethod().invoke(target, value);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set property " + property, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Failed to validate argument " + value + " for " + property);
}
}
static String getAlias(Argument argument) {
String alias = argument.alias();
if (alias.equals("")) {
alias = null;
}
return alias;
}
static String getName(Argument argument, Field field) {
String name = argument.value();
if (name.equals("")) {
name = field.getName();
}
return name;
}
static void setField(Class<?> type, Field field, Object target, Object value, String delimiter) {
makeAccessible(field);
try {
value = getValue(type, value, delimiter);
field.set(target, value);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set field " + field, iae);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Could not find constructor in class " + type.getName() + " that takes a string", e);
}
}
private static Object getValue(Class<?> type, Object value, String delimiter) throws NoSuchMethodException {
if (type != String.class && type != Boolean.class && type != Boolean.TYPE) {
if (type.isArray()) {
String string = (String) value;
String[] strings = string.split(delimiter);
type = type.getComponentType();
if (type == String.class) {
value = strings;
} else {
Object[] array = (Object[]) Array.newInstance(type, strings.length);
for (int i = 0; i < array.length; i++) {
array[i] = createValue(type, strings[i]);
}
value = array;
}
} else {
value = createValue(type, value);
}
}
return value;
}
private static Object createValue(Class<?> type, Object value) throws NoSuchMethodException {
Constructor<?> init = type.getDeclaredConstructor(String.class);
try {
value = init.newInstance(value);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert " + value + " to type " + type.getName(), e);
}
return value;
}
private static void makeAccessible(AccessibleObject ao) {
if (ao instanceof Member) {
Member member = (Member) ao;
if (!Modifier.isPublic(member.getModifiers())) {
ao.setAccessible(true);
}
}
}
}