package org.dcache.util.cli;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import java.io.Serializable;
import java.lang.reflect.AnnotatedElement;
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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import dmg.util.CommandException;
import dmg.util.CommandPanicException;
import dmg.util.CommandSyntaxException;
import dmg.util.CommandThrowableException;
import dmg.util.command.AnnotatedCommandHelpPrinter;
import dmg.util.command.AnsiHelpPrinter;
import dmg.util.command.Argument;
import dmg.util.command.GlobExpander;
import dmg.util.command.Command;
import dmg.util.command.CommandLine;
import dmg.util.command.ExpandWith;
import dmg.util.command.HelpFormat;
import dmg.util.command.Option;
import dmg.util.command.PlainHelpPrinter;
import org.dcache.util.Args;
import org.dcache.util.Glob;
import org.dcache.util.ReflectionUtils;
import static com.google.common.collect.Iterables.*;
import static java.util.Arrays.asList;
/**
* CommandExecutor for commands implemented as non-static inner
* classes implementing Callable and annotated with @Command.
*/
public class AnnotatedCommandExecutor implements CommandExecutor
{
private static final Function<Handler, Integer> GET_MAX_ARGS =
new Function<Handler,Integer>() {
@Override
public Integer apply(Handler handler)
{
return handler.getMaxArguments();
}
};
private static final ImmutableMap<HelpFormat, AnnotatedCommandHelpPrinter> HELP_PRINTERS =
ImmutableMap.<HelpFormat, AnnotatedCommandHelpPrinter>builder()
.put(HelpFormat.PLAIN, new PlainHelpPrinter())
.put(HelpFormat.ANSI, new AnsiHelpPrinter())
.build();
private static final Joiner AS_COMMA_LIST = Joiner.on(", ");
private final Object _parent;
private final Command _command;
private final Constructor<? extends Callable<? extends Serializable>> _constructor;
private final List<Handler> _handlers;
private final boolean _isDeprecated;
public AnnotatedCommandExecutor(Object parent, Command command,
Constructor<? extends Callable<? extends Serializable>> constructor)
{
_parent = parent;
_command = command;
_constructor = constructor;
Class<? extends Callable<? extends Serializable>> commandClass = _constructor.getDeclaringClass();
_handlers = createFieldHandlers(command, commandClass);
_isDeprecated = commandClass.getAnnotation(Deprecated.class) != null;
}
@Override
public boolean isDeprecated()
{
return _isDeprecated;
}
@Override
public boolean hasACLs()
{
return _command.acl().length > 0;
}
@Override
public String[] getACLs()
{
return _command.acl();
}
private AnnotatedCommandHelpPrinter getHelpPrinter(HelpFormat format)
{
AnnotatedCommandHelpPrinter printer = HELP_PRINTERS.get(format);
if (printer == null) {
printer = HELP_PRINTERS.get(HelpFormat.PLAIN);
}
return printer;
}
@Override
public String getHelpHint(HelpFormat format)
{
return getHelpPrinter(format).getHelpHint(_command, _constructor.getDeclaringClass());
}
@Override
public String getFullHelp(HelpFormat format)
{
return getHelpPrinter(format).getHelp(createInstance());
}
@Override
public Serializable execute(Args arguments) throws CommandException
{
Callable<? extends Serializable> command = createInstance();
try {
for (Handler handler: _handlers) {
handler.apply(command, arguments);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("This is a bug. Please notify " +
"support@dcache.org", e);
} catch (IllegalArgumentException e) {
throw new CommandSyntaxException(e.getMessage());
}
try {
return command.call();
} catch (CommandException e) {
throw e;
} catch (RuntimeException e) {
try {
/* We treat uncaught RuntimeExceptions other than
* those declared to be thrown by the method as
* bugs and propagate them.
*/
Method method = command.getClass().getMethod("call");
if (!ReflectionUtils.hasDeclaredException(method, e)) {
throw new CommandPanicException("Command failed: " + e.toString(), e);
}
throw new CommandThrowableException(
e.toString() + " from " + _command.name(), e);
} catch (NoSuchMethodException nsme) {
throw new RuntimeException(
"This is a bug. Please notify support@dcache.org", nsme);
}
} catch (Exception e) {
throw new CommandThrowableException(
e.toString() + " from " + _command.name(), e);
}
}
@Override
public AnnotatedElement getImplementation()
{
return _constructor.getDeclaringClass();
}
private Callable<? extends Serializable> createInstance()
{
try {
return _constructor.newInstance(_parent);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(
"This is a bug. Please notify support@dcache.org", e);
}
}
private static Handler createFieldHandler(Field field, Option option)
{
Class<?> type = field.getType();
if (type.isArray()) {
Function<String,Object> typeConverter =
createTypeConverter(type.getComponentType());
if (option.values().length > 0) {
typeConverter = new MultipleChoiceValidator(
typeConverter, asList(option.values()));
}
if (option.separator().isEmpty()) {
return new MultiValuedOptionHandler(field, typeConverter, option);
} else {
return new SplittingOptionHandler(field, typeConverter, option);
}
} else {
Function<String,Object> typeConverter = createTypeConverter(type);
if (option.values().length > 0) {
typeConverter = new MultipleChoiceValidator(
typeConverter, asList(option.values()));
}
return new OptionHandler(field, typeConverter, option);
}
}
private Handler createFieldHandler(Field field, Argument argument)
{
Class<?> type = field.getType();
if (type.isArray()) {
Function<String,Object> typeConverter =
createTypeConverter(type.getComponentType());
if (field.getAnnotation(ExpandWith.class) == null) {
return new MultiValuedArgumentHandler(field, typeConverter, argument);
} else {
return new ExpandingMultiValuedArgumentHandler(field, typeConverter, argument);
}
} else {
Function<String,Object> typeConverter = createTypeConverter(type);
return new ArgumentHandler(field, typeConverter, argument);
}
}
private static Handler createFieldHandler(Command command, Field field, CommandLine commandLine)
{
Class<?> type = field.getType();
if (type.isAssignableFrom(Args.class)) {
return new ArgsHandler(field);
} else if (type.isAssignableFrom(String.class)) {
return new CommandLineHandler(command, field);
} else {
throw new IllegalArgumentException("CommandLine annotation is only applicable to Args and String fields");
}
}
private List<Handler> createFieldHandlers(Command command, Class<? extends Callable<?>> clazz)
{
Set<String> optionNames = new HashSet<>();
boolean allowAnyOption = false;
List<Handler> handlers = Lists.newArrayList();
for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
for (Field field: c.getDeclaredFields()) {
Option option = field.getAnnotation(Option.class);
if (option != null) {
handlers.add(createFieldHandler(field, option));
optionNames.add(option.name());
}
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
handlers.add(createFieldHandler(field, argument));
}
CommandLine commandLine = field.getAnnotation(CommandLine.class);
if (commandLine != null) {
handlers.add(createFieldHandler(command, field, commandLine));
allowAnyOption |= commandLine.allowAnyOption();
}
}
}
int maxArgs = handlers.isEmpty() ? 0 :
Ordering.natural().max(transform(handlers, GET_MAX_ARGS));
if (maxArgs < Integer.MAX_VALUE) {
handlers.add(new MaxArgumentsHandler(maxArgs));
}
if (!allowAnyOption) {
handlers.add(new CheckOptionsKnownHandler(optionNames));
}
return handlers;
}
private static Function<String,Object> createTypeConverter(Class<?> type)
{
if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) {
return new BooleanTypeConverter();
} else if (Byte.class.equals(type) || Byte.TYPE.equals(type)) {
return new ByteTypeConverter();
} else if (Character.class.equals(type) || Character.TYPE.equals(type)) {
return new CharacterTypeConverter();
} else if (Double.class.equals(type) || Double.TYPE.equals(type)) {
return new DoubleTypeConverter();
} else if (Float.class.equals(type) || Float.TYPE.equals(type)) {
return new FloatTypeConverter();
} else if (Integer.class.equals(type) || Integer.TYPE.equals(type)) {
return new IntegerTypeConverter();
} else if (Long.class.equals(type) || Long.TYPE.equals(type)) {
return new LongTypeConverter();
} else if (Short.class.equals(type) || Short.TYPE.equals(type)) {
return new ShortTypeConverter();
} else if (String.class.equals(type)) {
return new StringTypeConverter();
} else if (type.isEnum()) {
return new EnumTypeConverter(type.asSubclass(Enum.class));
} else if (!type.isInterface() && !type.isAnnotation() && !type.isPrimitive()) {
try {
Method method = type.getMethod("valueOf", String.class);
if (Modifier.isStatic(method.getModifiers())) {
return new ValueOfTypeConverter(method);
}
} catch (NoSuchMethodException ignored) {
}
try {
return new StringConstructorTypeConverter(
type.getConstructor(String.class));
} catch (NoSuchMethodException ignored) {
}
}
throw new RuntimeException("This is a bug. Please notify " +
"support@dcache.org. Cannot convert to type " + type);
}
/**
* Implementations of this interface can process arguments and
* options represented as an Args object. A typical implementation
* will apply an argument or option to a command object's field
* using reflection.
*/
private interface Handler
{
/**
* Applies processing to the args parameter.
*
* May modify the fields of the command object.
*
* @param object A command object
* @param args Args object to which to apply processing
* @throws IllegalAccessException if reflection on a field
* of object fails due to lack of access
* @throws IllegalArgumentException If any of the options or
* arguments in args violates the constraints of the handler-
*/
void apply(Object object, Args args)
throws IllegalAccessException;
/**
* Maximum number of arguments consumed by this handler.
*
* @return maximum number of arguments or Integer.MAX_VALUE
* if there is no upper bound.
*/
int getMaxArguments();
}
/**
* Rejects arguments lists longer than a specified maximum.
*/
private static class MaxArgumentsHandler implements Handler
{
private final int _max;
public MaxArgumentsHandler(int max)
{
_max = max;
}
@Override
public int getMaxArguments()
{
return 0;
}
@Override
public void apply(Object object, Args args)
throws IllegalAccessException
{
if (args.argc() > _max) {
throw new IllegalArgumentException("Too many arguments: " +
Joiner.on(" ").join(args.getArguments()
.subList(_max, args.argc())));
}
}
}
/**
* Abstract base class for handlers that apply a value to a field
* of the command object.
*/
private abstract static class FieldHandler implements Handler
{
protected final Field _field;
protected Object _command;
public FieldHandler(Field field)
{
_field = field;
_field.setAccessible(true);
}
protected abstract Object getValue(Args args);
@Override
public void apply(Object command, Args args)
throws IllegalAccessException
{
_command = command;
Object value = getValue(args);
if (value != null) {
_field.set(command, value);
}
}
}
/**
* Maps single value arguments to a field annotated as an @Argument.
*/
private static class ArgumentHandler extends FieldHandler
{
private final Function<String,Object> _typeConverter;
private final Argument _argument;
public ArgumentHandler(Field field, Function<String,Object> typeConverter, Argument argument)
{
super(field);
_typeConverter = typeConverter;
_argument = argument;
}
@Override
public int getMaxArguments()
{
return (_argument.index() >= 0) ? _argument.index() + 1 : -_argument.index();
}
@Override
protected Object getValue(Args args)
{
int index = _argument.index();
if (index < 0) {
index += args.argc();
}
if (0 <= index && index < args.argc()) {
return _typeConverter.apply(args.argv(index));
} else if (_argument.required()) {
throw new IllegalArgumentException("Argument " + (index + 1) + " is required");
}
return null;
}
}
/**
* Maps arguments to an array field annotated as an @Argument.
*/
private static class MultiValuedArgumentHandler extends FieldHandler
{
protected final Function<String,Object> _typeConverter;
protected final Argument _argument;
public MultiValuedArgumentHandler(Field field,
Function<String, Object> typeConverter,
Argument argument)
{
super(field);
if (argument.index() < 0) {
throw new IllegalArgumentException("Negative index is not allowed for multi valued arguments");
}
_typeConverter = typeConverter;
_argument = argument;
}
@Override
public int getMaxArguments()
{
return Integer.MAX_VALUE;
}
@Override
protected Object getValue(Args args)
{
int index = _argument.index();
if (index < args.argc()) {
return buildArray(args);
} else if (_argument.required()) {
throw new IllegalArgumentException("Argument " + (index + 1) + " is required");
}
return null;
}
protected Object buildArray(Args args)
{
int index = _argument.index();
Class<?> type = _field.getType().getComponentType();
Object values = Array.newInstance(type, args.argc() - index);
for (int i = index; i < args.argc(); i++) {
Object value = _typeConverter.apply(args.argv(i));
Array.set(values, i - index, value);
}
return values;
}
}
/**
* Map arguments to an array after expanding any that are Globs.
*/
private class ExpandingMultiValuedArgumentHandler extends MultiValuedArgumentHandler
{
private final ExpandWith _expand;
public ExpandingMultiValuedArgumentHandler(Field field,
Function<String, Object> typeConverter,
Argument argument)
{
super(field, typeConverter, argument);
_expand = field.getAnnotation(ExpandWith.class);
}
private GlobExpander<String> createNonstaticExpander(Object parent)
throws InstantiationException,
IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
try {
Constructor<? extends GlobExpander<String>> constructor =
_expand.value().getDeclaredConstructor(parent.getClass());
constructor.setAccessible(true);
return constructor.newInstance(parent);
} catch (NoSuchMethodException e) {
return null;
}
}
private GlobExpander<String> createStaticExpander()
throws NoSuchMethodException, InstantiationException,
IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
Constructor<? extends GlobExpander<String>> constructor =
_expand.value().getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
private GlobExpander<String> createExpander()
{
GlobExpander<String> expander;
try {
expander = createNonstaticExpander(_command); // as subclass of @Command class
if (expander == null) {
expander = createNonstaticExpander(_parent); // as sibling of @Command class
}
if (expander == null) {
expander = createStaticExpander(); // as static method
}
if (expander == null) {
throw new IllegalArgumentException("Cannot find constructor for " + _expand.value());
}
} catch (InstantiationException | NoSuchMethodException | IllegalAccessException |
IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException("Unable to build GlobExpander: " +
e.toString(), e);
}
return expander;
}
private Object asArrayForField(List<String> values)
{
Class<?> type = _field.getType().getComponentType();
Object array = Array.newInstance(type, values.size());
for (int i = 0; i < values.size(); i++) {
Array.set(array, i, _typeConverter.apply(values.get(i)));
}
return array;
}
@Override
protected Object buildArray(Args args)
{
GlobExpander<String> expander = null;
List<String> values = new ArrayList<>();
for (int i = _argument.index(); i < args.argc(); i++) {
String argument = args.argv(i);
if (Glob.isGlob(argument)) {
if (expander == null) {
expander = createExpander();
}
List<String> expansion = expander.expand(new Glob(argument));
if (expansion.isEmpty()) {
values.add(argument);
} else {
values.addAll(expansion);
}
} else {
values.add(argument);
}
}
return asArrayForField(values);
}
}
/**
* Maps a single value option to a field annotated as an @Option.
*/
private static class OptionHandler extends FieldHandler
{
private final Function<String,Object> _typeConverter;
private final Option _option;
public OptionHandler(Field field, Function<String,Object> typeConverter,
Option option)
{
super(field);
_typeConverter = typeConverter;
_option = option;
}
@Override
public int getMaxArguments()
{
return 0;
}
@Override
protected Object getValue(Args args)
{
String value = args.getOption(_option.name());
if (value != null) {
return _typeConverter.apply(value);
} else if (_option.required()) {
throw new IllegalArgumentException("Option " + _option.name() + " is required");
}
return null;
}
}
/**
* Maps an option list to an array field annotated as an @Option.
*/
private static class MultiValuedOptionHandler extends FieldHandler
{
private final Function<String,Object> _typeConverter;
private final Option _option;
public MultiValuedOptionHandler(Field field, Function<String,Object> typeConverter,
Option option)
{
super(field);
_typeConverter = typeConverter;
_option = option;
}
@Override
public int getMaxArguments()
{
return 0;
}
@Override @SuppressWarnings("unchecked")
protected Object getValue(Args args)
{
ImmutableList<String> values = args.getOptions(_option.name());
if (!values.isEmpty()) {
return toArray(transform(values, _typeConverter),
(Class) _field.getType().getComponentType());
} else if (_option.required()) {
throw new IllegalArgumentException("Option " + _option.name() + " is required");
}
return null;
}
}
/**
* Maps an option list to an array field annotated as an @Option.
* Option values are split into elements according to a separator
* string.
*/
private static class SplittingOptionHandler extends FieldHandler
{
private final Function<String,Object> _typeConverter;
private final Option _option;
private final Splitter _splitter;
public SplittingOptionHandler(Field field,
Function<String,Object> typeConverter,
Option option)
{
super(field);
_typeConverter = typeConverter;
_option = option;
_splitter = Splitter.on(option.separator());
}
@Override
public int getMaxArguments()
{
return 0;
}
@Override @SuppressWarnings("unchecked")
protected Object getValue(Args args)
{
ImmutableList<String> values = args.getOptions(_option.name());
if (!values.isEmpty()) {
List<String> fragments = Lists.newArrayList();
for (String value: values) {
if (!value.isEmpty()) {
addAll(fragments, _splitter.split(value));
}
}
return toArray(transform(fragments, _typeConverter),
(Class) _field.getType().getComponentType());
} else if (_option.required()) {
throw new IllegalArgumentException("Option " + _option.name() + " is required");
}
return null;
}
}
/**
* Assigns the entire Args object to a field.
*/
private static class ArgsHandler extends FieldHandler
{
private ArgsHandler(Field field)
{
super(field);
}
@Override
protected Object getValue(Args args)
{
return args;
}
@Override
public int getMaxArguments()
{
return 0;
}
}
/**
* Assigns the command line to a field.
*/
private static class CommandLineHandler extends FieldHandler
{
private final String command;
private CommandLineHandler(Command command, Field field)
{
super(field);
this.command = command.name();
}
@Override
protected Object getValue(Args args)
{
return (args.optc() == 0 && args.argc() == 0) ? command : (command + " " + args);
}
@Override
public int getMaxArguments()
{
return 0;
}
}
/**
* Prevents running a command if user supplied any options that are unknown.
*/
private static class CheckOptionsKnownHandler implements Handler
{
private final Set<String> _known;
private CheckOptionsKnownHandler(Set<String> names)
{
_known = names;
}
@Override
public void apply(Object object, Args args) throws IllegalAccessException
{
Set<String> supplied = args.options().keySet();
Set<String> unknown = Sets.difference(supplied, _known);
if (!unknown.isEmpty()) {
throw new IllegalArgumentException("Unknown option" +
(unknown.size() > 1 ? "s" : "") + ": " +
AS_COMMA_LIST.join(unknown));
}
}
@Override
public int getMaxArguments()
{
return Integer.MAX_VALUE;
}
}
/**
* A wrapper for other functions, which restricts the domain of the function
* to a finite set of prespecified values.
*/
private static class MultipleChoiceValidator implements Function<String,Object>
{
private final Function<String,Object> _inner;
private final List<String> _values;
public MultipleChoiceValidator(Function<String,Object> inner, List<String> values)
{
_inner = inner;
_values = values;
}
@Override
public Object apply(String value)
{
if (!_values.contains(value)) {
throw new IllegalArgumentException("Invalid value: " + value);
}
return _inner.apply(value);
}
}
/**
* A function from String to Boolean.
*/
private static class BooleanTypeConverter implements Function<String,Object>
{
@Override
public Boolean apply(String value)
{
if ("true".equalsIgnoreCase(value) || value.isEmpty()) {
return Boolean.TRUE;
} else if ("false".equalsIgnoreCase(value)) {
return Boolean.FALSE;
} else {
throw new IllegalArgumentException("Invalid value for boolean: " + value);
}
}
}
/**
* A function from String to Byte.
*/
private static class ByteTypeConverter implements Function<String,Object>
{
@Override
public Byte apply(String value)
{
return Byte.valueOf(value);
}
}
/**
* A function from String to Character.
*/
private static class CharacterTypeConverter implements Function<String,Object>
{
@Override
public Character apply(String value)
{
return value.charAt(0);
}
}
/**
* A function from String to Double.
*/
private static class DoubleTypeConverter implements Function<String,Object>
{
@Override
public Double apply(String value)
{
return Double.valueOf(value);
}
}
/**
* A function from String to Float.
*/
private static class FloatTypeConverter implements Function<String,Object>
{
@Override
public Float apply(String value)
{
return Float.valueOf(value);
}
}
/**
* A function from String to Integer.
*/
private static class IntegerTypeConverter implements Function<String,Object>
{
@Override
public Integer apply(String value)
{
return Integer.decode(value);
}
}
/**
* A function from String to Long.
*/
private static class LongTypeConverter implements Function<String,Object>
{
@Override
public Long apply(String value)
{
return Long.decode(value);
}
}
/**
* A function from String to Short.
*/
private static class ShortTypeConverter implements Function<String,Object>
{
@Override
public Short apply(String value)
{
return Short.decode(value);
}
}
/**
* Identity function from String to String.
*/
private static class StringTypeConverter implements Function<String,Object>
{
@Override
public String apply(String value)
{
return value;
}
}
/**
* A function from String to a class with a String constructor.
*/
private static class StringConstructorTypeConverter implements Function<String,Object>
{
private Constructor<?> _constructor;
public StringConstructorTypeConverter(Constructor<?> constructor)
{
_constructor = constructor;
}
@Override
public Object apply(String value)
{
try {
return _constructor.newInstance(value);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
Throwables.throwIfUnchecked(t);
throw new IllegalArgumentException(t.getMessage(), t);
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("This is a bug. Please notify support@dcache.org", e);
}
}
}
/**
* A function from String to a class with a static valueOf factory method.
*/
private static class ValueOfTypeConverter implements Function<String,Object>
{
private Method _method;
public ValueOfTypeConverter(Method method)
{
_method = method;
}
@Override
public Object apply(String value)
{
try {
return _method.invoke(null, value);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
Throwables.throwIfUnchecked(t);
throw new IllegalArgumentException(t.getMessage(), t);
} catch (IllegalAccessException e) {
throw new RuntimeException("This is a bug. Please notify support@dcache.org", e);
}
}
}
/**
* A function from String to an enum class.
*/
private static class EnumTypeConverter implements Function<String,Object>
{
private Class<? extends Enum> _type;
public EnumTypeConverter(Class<? extends Enum> type)
{
_type = type;
}
@Override
public Object apply(String value)
{
return Enum.valueOf(_type, value.toUpperCase());
}
}
}