
il.org.spartan.external.External Maven / Gradle / Ivy
Show all versions of cli Show documentation
/**
* Copyright (c) 2005, Sam Pullara. All Rights Reserved. You may modify and
* redistribute as long as this attribution remains. Modernized and polished
* by Yossi Gil [email protected], 2011. פרופ' יוסי גיל (c)
Original
* copyright remains. Original version can be found here.
*/
package il.org.spartan.external;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
/**
* Annotation for static
and non-static
* data members whose value can be set, externally, i.e., not by usual
* initialization or via a setter, but from command line arguments.
The
* value of such an annotated field can be extracted from the command line
* arguments by invoking function
* {@link Introspector#extract(String[], Object...)} or function
* {@link Introspector#extract(List, Object...)}. The value can also be
* extracted from @link{java.util.Properties} by means of
* {@link Introspector#extract(Properties, Object...)}.
*
* @author Sam Pullara.
* @author Yossi Gil {@literal }
* @since 2011-08-20
* @see Introspector
*/
@Documented //
@Retention(RetentionPolicy.RUNTIME) //
@Target({ ElementType.FIELD, ElementType.METHOD }) //
public @interface External {
/**
* A description of the argument that will appear in the usage method
*/
String value() default "";
/**
* Further description of the argument that will appear in the usage method
* (if both this and the and the #value field are present, then the full
* description is obtained by their concatenation, first #value and then this
* field.
*/
String description() default "";
/**
* Optional name of this command line option, overriding the data member name.
*/
String name() default "";
/**
* An alias for this option
*/
String alias() default "";
/**
* If true, then the option must be set for the parser not to fail
*/
boolean required() default false;
/**
* A delimiter for arguments that are multi-valued.
*/
String delimiter() default ",";
/**
* Annotation for static
and non-
* static
array data member whose value is to be set from
* these command lines which were not consumed to fill data members annotated
* as {@link External}. Thus, a call to function
* {@link Introspector#extract(String[], Object...)} (or function
* {@link Introspector#extract(List, Object...)} for this matter), will
* initialize any data member marked as {@link Residue} with the
* residual arguments, i.e., those which were not options. As usual, the
* array component type must must have a constructor which takes a single
* {@link String} argument.
*
* @author Yossi Gil
* @since 2011-08-20
*/
@Documented //
@Retention(RetentionPolicy.RUNTIME) //
@Target({ ElementType.FIELD, ElementType.METHOD }) //
public @interface Residue {
//
}
public static class Introspector {
/**
* Parse a set of arguments and populate the target with the appropriate
* values.
*
* @param args
* The arguments you want to parse and populate
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return The list of arguments that were not consumed
* @throws Introspector.Argument.ParsingError
* in case the command line arguments could not be parsed
* successfully, i.e., user provided incorrect input.
* @throws Introspector.Argument.ReflectionError
* in case the extracted value could not be injected into the its
* targets, which is typically a result of misuse of this package,
* e.g., applying an {@link External} annotation to a
* final
field.
*/
public static List extract(final String[] args, final Object... targets) {
return extract(cloneAsList(args), targets);
}
/**
* Extract <keyword,value>
pairs from a list of arguments, as specified by
* the {@link External} decorated fields of given object; set these fields,
* and return the remaining arguments
*
* @param arguments
* command line arguments
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return the command line arguments, where the <keyword,value>
pairs are
* removed.
* @throws Introspector.Argument.ParsingError
* in case the command line arguments could not be parsed
* successfully, i.e., user provided incorrect input.
* @throws Introspector.Argument.ReflectionError
* in case the extracted value could not be injected into the its
* targets, which is typically a result of misuse of this package,
* e.g., applying an {@link External} annotation to a
* final
field.
*/
public static List extract(final List arguments, final Object... targets) {
final List $ = new Introspector().extractInto(arguments, targets);
residue($, targets);
return $;
}
/**
* Generate usage information based on annotations.
*
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return Usage string
*/
public static String usage(final Object... targets) {
return usage(targets[0], "", targets);
}
/**
* Generate usage information based on the target annotations.
*
* @param main
* an instance or a class object, specifying the main class, from
* which the application is invoked.
* @param usage
* additional usage information
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found.
* @return Usage string
*/
public static String usage(final Object main, final String usage, final Object... targets) {
final StringBuilder $ = new StringBuilder("Usage: java " + fullName(main) + " " + usage + "\n");
for (final Object target : targets)
usage($, target, getClass(target));
return $.toString();
}
/**
* Generate usage information based on the target annotations.
*
* @param usage
* additional usage information, usually pertaining to the
* non-options
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return Usage string
*/
public static String usage(final String usage, final Object... targets) {
return usage(targets[0], usage, targets);
}
/**
* Prints to the standard error stream usage information text based on the
* target annotations, and abort.
*
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
*/
public static void usageErrorExit(final Object... targets) {
usageErrorExit("", targets);
}
/**
* Prints to the standard error stream a usage information text based on the
* target annotations, and abort.
*
* @param usage
* additional usage information
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
*/
public static void usageErrorExit(final String usage, final Object... targets) {
System.err.print(usage(usage, targets));
System.exit(1);
}
/**
* Generate a pretty printed string describing the settings of all
* {@link External} annotated members specified in the parameter. Each of
* set objects is formatted as its short class name, followed by tab
* indented lines, each in a key=value form.
*
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return a pretty print string describing the current settings of the
* parameter.
*/
public static String settings(final Object... targets) {
final StringBuilder $ = new StringBuilder();
for (final Object target : targets) {
$.append(shortName(target) + ":\n");
for (final Entry e : toOrderedMap(target).entrySet())
$.append("\t").append(e.getKey()).append("=").append(e.getValue()).append("\n");
}
return $.toString();
}
/**
* Convert the settings in the parameter as a set of <String, String>
* entries, in the order that they were defined.
*
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return a {@link Set} of {@link java.util.Map.Entry} objects, each
* containing a key,value pair.
*/
public static Map toOrderedMap(final Object... targets) {
final Map $ = new LinkedHashMap();
for (final Object target : targets) {
final Class extends Object> c = getClass(target);
for (final PropertyDescriptor p : descriptors(c))
addEntry($, target, p);
for (final Field f : fields(c))
addEntry($, target, f);
for (final Method m : getters(c))
addEntry($, target, m);
}
return $;
}
private static void addEntry(final Map es, final Object target, final Field f) {
final Argument a = Argument.make(f);
if (a == null || es.containsKey(a.name))
return;
es.put(a.name, a.asString(a.get(target, f)));
}
private static void addEntry(final Map es, final Object target, final Method m) {
final Argument a = Argument.make(m);
if (a == null || es.containsKey(a.name))
return;
es.put(a.name, a.asString(a.get(target, m)));
}
private static void addEntry(final Map es, final Object target, final PropertyDescriptor p) {
final Argument a = Argument.make(p);
if (a == null || es.containsKey(a.name))
return;
es.put(a.name, a.asString(a.get(target, p)));
}
/**
* Convert the settings in the parameter to a {@link Properties} object.
*
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @return a {@link Properties} object with the settings of the parameter.
*/
public static Properties toProperties(final Object... targets) {
final Properties $ = new Properties();
for (final Object target : targets) {
final Class extends Object> c = getClass(target);
for (final Field f : fields(c))
addProperties($, target, f);
for (final PropertyDescriptor p : descriptors(c))
addProperties($, target, p);
}
return $;
}
private final List errors = new ArrayList();
private static void addProperties(final Properties m, final Object target, final Field f) {
final Argument a = Argument.make(f);
if (a == null)
return;
m.put(a.name, a.asString(a.get(target, f)));
}
private static void addProperties(final Properties ps, final Object target, final PropertyDescriptor p) {
final Argument a = Argument.make(p);
if (a == null)
return;
ps.put(a.name, a.asString(a.get(target, p)));
}
private static Class extends Object> getClass(final Object o) {
return o instanceof Class ? (Class>) o : o.getClass();
}
private static String fullName(final Object o) {
return getClass(o).getName();
}
static String shortName(final Object o) {
return getClass(o).getSimpleName();
}
private static void usage(final StringBuilder b, final Object target, final Class extends Object> c) {
try {
for (final Field f : fields(c))
b.append(usage(target, f));
for (final PropertyDescriptor pd : descriptors(c))
b.append(usage(target, pd));
} catch (final Error _) {
// No point in treating any errors while collecting usage
// information
}
}
private static String usage(final Object target, final PropertyDescriptor p) {
final Argument a = Argument.make(p);
return a == null ? "" : a.usage(a.get(target, p)) + "\n";
}
private static String usage(final Object target, final Field f) {
final Argument $ = Argument.make(f);
return $ == null ? "" : $.usage($.get(target, f)) + "\n";
}
private static ArrayList cloneAsList(final String[] args) {
return new ArrayList(Arrays.asList(args));
}
private static void residue(final List arguments, final Object[] targets) {
for (final Object target : targets)
if (target instanceof Class)
residueIntoClass(arguments, (Class>) target);
else
resiudueIntoInstance(target, arguments);
}
private static void resiudueIntoInstance(final Object target, final List arguments) {
for (final Field f : fields(target.getClass()))
residue(target, f, arguments);
}
private static void residueIntoClass(final List arguments, final Class> base) {
for (Class> c = base; c != null; c = c.getSuperclass())
for (final Field f : c.getDeclaredFields())
residue(c, f, arguments);
}
private static void residue(final Object target, final Field f, final List arguments) {
if (f.getAnnotation(Residue.class) == null)
return;
final Argument a = Argument.makeResidue(f);
if (a != null)
a.set(f, target, arguments);
}
private List extractInto(final List arguments, final Object... targets) {
for (final Object target : targets)
if (target instanceof Class)
extractIntoClass((Class>) target, arguments);
else
extractIntoInstance(target, arguments);
check(arguments);
wrapErrors(targets);
return arguments;
}
private void extractIntoClass(final Class> base, final List arguments) {
for (final PropertyDescriptor p : descriptors(base))
extractInto(base, p, arguments);
for (Class> c = base; c != null; c = c.getSuperclass())
for (final Field f : c.getDeclaredFields())
extractInto(c, f, arguments);
}
private void extractIntoInstance(final Object target, final List arguments) {
final Class extends Object> c = target.getClass();
for (final PropertyDescriptor p : descriptors(c))
extractInto(target, p, arguments);
for (final Field f : fields(c))
extractInto(target, f, arguments);
}
private static PropertyDescriptor[] descriptors(final Class extends Object> c) {
try {
return java.beans.Introspector.getBeanInfo(c).getPropertyDescriptors();
} catch (final IntrospectionException _) { // Ignore errors of this
// sort
return new PropertyDescriptor[0];
}
}
private void wrapErrors(final Object... targets) {
for (final Error e : errors)
System.err.println(e.getMessage());
if (errors.size() != 0) {
System.err.println(usage(targets));
throw errors.get(0);
}
}
private void extractInto(final Object target, final Field f, final List arguments) {
try {
final Argument a = Argument.make(f);
if (a != null)
a.set(f, target, a.extractValue(arguments));
} catch (final Error e) {
errors.add(e);
}
}
private void extractInto(final Object target, final PropertyDescriptor p, final List arguments) {
try {
final Argument a = Argument.make(p);
if (a != null)
a.set(p, target, a.extractValue(arguments));
} catch (final Error e) {
errors.add(e);
}
}
/**
* Parse properties instead of String arguments. Any additional arguments
* need to be passed some other way. This is often used in a second pass
* when the property filename is passed on the command line. Because of
* required properties you must be careful to set them all in the property
* file.
*
* @param ps
* The properties that contain the arguments
* @param targets
* An array of items, each being an instance or a class object, in
* which {@link External} specifications are to be found. The first
* element is interpreted also as the specifier of the main class.
* @throws Introspector.Argument.ParsingError
* in case the command line arguments could not be parsed
* successfully, i.e., user provided incorrect input.
* @throws Introspector.Argument.ReflectionError
* in case the extracted value could not be injected into the its
* targets, which is typically a result of misuse of this package,
* e.g., applying an {@link External} annotation to a
* final
field.
*/
public static void extract(final Properties ps, final Object... targets) {
new Introspector().extractInto(ps, targets);
}
private void extractInto(final Properties ps, final Object[] targets) {
for (final Object target : targets)
extractInto(ps, target);
wrapErrors(targets);
}
private static void extractInto(final Properties ps, final Object target) {
final Class> c = getClass(target);
for (final Field field : fields(c))
extract(target, field, ps);
for (final PropertyDescriptor p : descriptors(c))
extract(target, p, ps);
}
private static List fields(final Class> base) {
final ArrayList $ = new ArrayList();
for (Class> c = base; c != null; c = c.getSuperclass())
for (final Field f : c.getDeclaredFields())
$.add(f);
return $;
}
private static List getters(final Class> base) {
final ArrayList $ = new ArrayList();
for (Class> c = base; c != null; c = c.getSuperclass())
for (final Method m : c.getDeclaredMethods())
if (isGetter(m))
$.add(m);
return $;
}
private static boolean isGetter(final Method m) {
return m.getParameterTypes().length == 0 && m.getReturnType() != Void.TYPE;
}
private static void extract(final Object target, final Field f, final Properties ps) {
final Argument a = Argument.make(f);
if (a == null)
return;
a.set(f, target, a.extractValue(ps));
}
private static void extract(final Object target, final PropertyDescriptor p, final Properties ps) {
final Argument a = Argument.make(p);
if (a == null)
return;
a.set(p, target, a.extractValue(ps));
}
private void check(final List arguments) {
for (final String argument : arguments)
if (argument.startsWith("-"))
errors.add(new UnrecognizedOption(argument));
}
private static abstract class Error extends RuntimeException {
public Error(final String message, final Throwable cause) {
super(message, cause);
}
public Error(final String message) {
super(message);
}
private static final long serialVersionUID = 1L;
}
public static final class NonArray extends Error {
NonArray(final String field) {
super(field + ": is not an array type");
}
private static final long serialVersionUID = 1L;
}
private static abstract class ArgumentError extends Error {
public ArgumentError(final String option, final String error, final Throwable cause) {
super(option + ": " + error, cause);
}
public ArgumentError(final String option, final String error) {
super(option + ": " + error);
}
private static final long serialVersionUID = 1L;
}
public static class UnrecognizedOption extends ArgumentError {
public UnrecognizedOption(final String argument) {
super(argument, "Unrecognized option");
}
private static final long serialVersionUID = 1L;
}
static class Argument {
public final String name;
public final boolean required;
public final String alias;
private final String description;
private final String delimiter;
private static final String PREFIX = "-";
public final Class> type;
static Argument make(final Field f) {
return make(f.getAnnotation(External.class), f.getName(), f.getType());
}
static Argument make(final Method m) {
return make(m.getAnnotation(External.class), m.getName(), m.getReturnType());
}
static Argument makeResidue(final Field f) {
if (f.getType().getComponentType() == null)
throw new NonArray(f.getName());
return new Argument(f.getName(), f.getType());
}
String asString(final Object o) {
return type.isArray() ? arrayValue((Object[]) o) : "" + o;
}
private String arrayValue(final Object[] os) {
final StringBuilder $ = new StringBuilder();
for (final Object o : os)
$.append($.length() == 0 ? "" : delimiter).append(o);
return $.toString();
}
static Argument make(final PropertyDescriptor p) {
final Method m = p.getWriteMethod();
return m == null ? null : Argument.make(m.getAnnotation(External.class), p.getName(), p.getPropertyType());
}
static Argument make(final External a, final String name, final Class> type) {
return a == null ? null : new Argument(a, name, type);
}
private Argument(final String name, final Class> type) {
this(name, type, null, false, null, null);
}
private Argument(final External a, final String defaultName, final Class> type) {
this(defaultsTo(a.name(), defaultName), //
type, defaultsTo(a.alias(), null), //
a.required(), //
a.value() + a.description(), //
a.delimiter());
}
private static String defaultsTo(final String value, final String defaultValue) {
return empty(value) ? defaultValue : value;
}
private Argument(final String name, final Class> type, final String alias, final boolean required, final String description,
final String delimiter) {
this.type = type;
this.required = required;
this.alias = alias;
this.description = description;
this.delimiter = delimiter;
this.name = name;
}
private Iterator find(final List arguments) {
for (final Iterator i = arguments.iterator(); i.hasNext();)
if (equals(i.next()))
return i;
return null;
}
String extractValue(final Properties ps) {
if (ps.get(name) != null)
return (String) ps.get(name);
if (alias != null)
return (String) ps.get(alias);
return checkRequired();
}
String extractValue(final List arguments) {
final Iterator i = find(arguments);
if (i == null)
return checkRequired();
i.remove();
if (find(arguments) != null)
throw new DuplicateOption();
return extractValue(i);
}
private boolean equals(final String text) {
if (!text.startsWith(PREFIX))
return false;
final String sansPrefix = text.substring(PREFIX.length());
return sansPrefix.equals(name) || alias != null && sansPrefix.equals(alias);
}
private String extractValue(final Iterator i) {
if (isBoolean())
return "true";
if (!i.hasNext())
throw new MissingValueForOption();
final String $ = i.next();
i.remove();
return $;
}
String checkRequired() {
if (required)
throw new RequiredOption();
return null;
}
void set(final Field f, final Object target, final String value) {
if (value == null)
return;
set(f, target, asObject(value));
}
void set(final Field f, final Object target, final List values) {
set(f, target, asArrayObject(type.getComponentType(), values));
}
void set(final Field f, final Object target, final Object value) {
f.setAccessible(true);
try {
f.set(target, value);
} catch (final ExceptionInInitializerError e) {
throw new FieldInitializationError(f, value, e);
} catch (final IllegalAccessException e) {
if (Modifier.isFinal(f.getModifiers()))
throw new FieldIsFinal(f);
} catch (final IllegalArgumentException e) {
throw new WrongTarget(f, value, e);
}
}
void set(final PropertyDescriptor p, final Object target, final String value) {
if (value == null)
return;
set(p, target, asObject(value));
}
private void set(final PropertyDescriptor p, final Object target, final Object value) {
try {
p.getWriteMethod().invoke(target, value);
} catch (final InvocationTargetException e) {
throw new FieldConversionError(p, value, e);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
Object get(final Object o, final PropertyDescriptor p) {
final Method m = p.getReadMethod();
if (m == null)
return null;
try {
return m.invoke(o, (Object[]) null);
} catch (final Exception e) {
throw new FieldConversionError(p, e);
}
}
Object get(final Object o, final Method m) {
m.setAccessible(true);
try {
return m.invoke(o, (Object[]) null);
} catch (final Exception e) {
throw new FieldConversionError(m, e);
}
}
Object get(final Object o, final Field f) {
f.setAccessible(true);
try {
return f.get(o);
} catch (final Throwable e) {
throw new FieldUnreadable(f, e);
}
}
private static boolean empty(final String s) {
return s == null || s.equals("");
}
private Object asObject(final String value) {
if (isBoolean())
return Boolean.TRUE;
if (type == String.class)
return value;
if (type.isArray())
return asArrayObject(type.getComponentType(), value);
return instantiate(type, value);
}
private boolean isBoolean() {
return type == Boolean.class || type == Boolean.TYPE;
}
private Object asArrayObject(final Class> c, final String value) {
final String[] strings = value.split(delimiter);
if (c.isPrimitive())
return asPrimitivesArrayObject(c, strings);
if (c == String.class)
return strings;
final Object[] $ = (Object[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = instantiate(c, strings[i]);
return $;
}
private Object asPrimitivesArrayObject(final Class> c, final String[] strings) {
if (c == byte.class) {
final byte[] $ = (byte[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = ((Byte) instantiate(c, strings[i])).byteValue();
return $;
}
if (c == short.class) {
final short[] $ = (short[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = ((Short) instantiate(c, strings[i])).shortValue();
return $;
}
if (c == int.class) {
final int[] $ = (int[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = ((Integer) instantiate(c, strings[i])).intValue();
return $;
}
if (c == long.class) {
final long[] $ = (long[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = ((Long) instantiate(c, strings[i])).longValue();
return $;
}
if (c == float.class) {
final float[] $ = (float[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = ((Float) instantiate(c, strings[i])).floatValue();
return $;
}
if (c == double.class) {
final double[] $ = (double[]) Array.newInstance(c, strings.length);
for (int i = 0; i < $.length; i++)
$[i] = ((Double) instantiate(c, strings[i])).doubleValue();
return $;
}
return null;
}
private Object asArrayObject(final Class> c, final List values) {
final Object[] $ = (Object[]) Array.newInstance(c, values.size());
for (int i = 0; i < $.length; i++)
$[i] = instantiate(c, values.get(i));
return $;
}
private Object instantiate(final Class> c, final String value) {
try {
if (c == byte.class)
return new Byte(value);
if (c == short.class)
return new Short(value);
if (c == int.class)
return new Integer(value);
if (c == long.class)
return new Long(value);
if (c == double.class)
return new Double(value);
if (c == float.class)
return new Float(value);
} catch (final NumberFormatException e) {
throw new NumericParsingError(value, e);
}
return c.isEnum() ? findEnum(c, value) : instantiate(getStringConstructor(c), value);
}
private Object findEnum(final Class> c, final String value) {
try {
return c.getDeclaredMethod("valueOf", String.class).invoke(null, value);
} catch (final InvocationTargetException e) {
throw new InvalidEnumValue(value);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private Object instantiate(final Constructor> c, final String value) {
try {
return c.newInstance(value);
} catch (final Exception e) {
throw new FieldConversionError(c, value, e);
}
}
private Constructor> getStringConstructor(final Class> c) {
try {
return c.getDeclaredConstructor(String.class);
} catch (final NoSuchMethodException e) {
throw new ConstructorWithSingleStringArgumentMissing(c, e);
}
}
String usage(final Object defaultValue) {
final StringBuilder $ = optionName();
$.append(" [").append(typeName()).append("] ").append(description);
if (defaultValue == null)
$.append(defaultValue);
else {
$.append(" (");
if (type.isArray()) {
final List