package ilarkesto.console;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Map.Entry;
public class ConsoleApp {
private final Map<String, Command> commands = new LinkedHashMap<String, Command>();
public enum ExecutionMode {
RUN_ONCE, ASK_TO_CONTINUE, RUN_UNTIL_EXIT
};
private ExecutionMode mode = ExecutionMode.RUN_ONCE;
private boolean exit = false;
private boolean showParameterNames = false;
public static ConsoleApp fromClass(Class<?> clazz) {
ConsoleApp app = new ConsoleApp();
ArrayList<Method> methods = new ArrayList<Method>(Arrays.asList(clazz.getDeclaredMethods()));
Map<String, ArrayList<Method>> methodsByName = new HashMap<String, ArrayList<Method>>();
for (Method m : methods) {
if (isPublicAndStatic(m) && !isMain(m)) {
// sort into buckets by name
if (methodsByName.get(m.getName()) == null) methodsByName.put(m.getName(), new ArrayList<Method>());
methodsByName.get(m.getName()).add(m);
}
}
// add commands
for (Entry<String, ArrayList<Method>> entry : methodsByName.entrySet()) {
Method[] methodArray = new Method[entry.getValue().size()];
entry.getValue().toArray(methodArray);
app.addCommand(entry.getKey(), methodArray);
}
return app;
}
private static boolean isPublicAndStatic(Method m) {
int modifiers = m.getModifiers();
return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
}
private static boolean isMain(Method m) {
return isPublicAndStatic(m) && m.getName().equals("main");
}
public void addCommand(String name, Method... methods) {
addCommand(null, name, methods);
}
private void addCommand(Object object, String name, Method... methods) {
if (methods.length <= 0) throw new IllegalArgumentException("At least one method must be added.");
if (methods.length > 1 && !assertOverloading(methods))
throw new IllegalArgumentException("Only overloaded actions should be added in one addAction call.");
if (commands.containsKey(name))
throw new IllegalArgumentException("There is already a command named '" + name + "'.");
commands.put(name, new Command(object, methods));
}
private boolean assertOverloading(Method[] methods) {
Class<?> clazz = methods[0].getDeclaringClass();
String name = methods[0].getName();
for (Method m : methods) {
if (clazz != m.getDeclaringClass() || !name.equals(m.getName())) return false;
}
return true;
}
public void printUsage() {
int maxParams = getMaxNumberOfParams();
ConsoleTable table = new ConsoleTable();
for (String cmd : commands.keySet()) {
Command command = commands.get(cmd);
Iterator<Method> it = command.getMethods().iterator();
Method m = null;
while (it.hasNext()) {
table.addRow(m == null ? cmd : "");
m = it.next();
if (showParameterNames) {
table.appendRow(getParameterNames(m));
} else {
table.appendRow(getSimpleParameterTypes(m));
}
table.appendRowFromColumn(maxParams + 2, getCallDescription(m));
}
}
System.out.println(table);
}
public ConsoleApp showParameterNames() {
this.showParameterNames = true;
return this;
}
private String[] getSimpleParameterTypes(Method method) {
List<String> types = new LinkedList<String>();
for (Class<?> clazz : method.getParameterTypes()) {
types.add(clazz.getSimpleName());
}
String[] result = new String[types.size()];
types.toArray(result);
return result;
}
private String[] getParameterNames(Method method) {
String[] names = new String[method.getParameterTypes().length];
Annotation[][] annotations = method.getParameterAnnotations();
outer: for (int i = 0; i < names.length; i++) {
for (Annotation a : annotations[i]) {
if (a instanceof ParameterDescription) {
names[i] = ((ParameterDescription) a).name();
continue outer;
}
throw new IllegalStateException(
"Cannot use parameter names, because parameter annotations are missing.");
}
}
return names;
}
private String getCallDescription(Method method) {
CallDescription desc = method.getAnnotation(CallDescription.class);
return (desc != null) ? desc.text() : "";
}
private int getMaxNumberOfParams() {
int n = 0;
for (Command command : commands.values()) {
n = Math.max(n, command.getMaxNumberOfParams());
}
return n;
}
public ConsoleApp setExecutionMode(ExecutionMode mode) {
if (this.mode == ExecutionMode.RUN_UNTIL_EXIT && this.mode != mode) removeExitCommand();
if (mode == ExecutionMode.RUN_UNTIL_EXIT) addExitCommand();
this.mode = mode;
return this;
}
private void addExitCommand() {
try {
addCommand(this, "exit", ConsoleApp.class.getDeclaredMethod("exit"));
} catch (NoSuchMethodException e) {
// should never happen
throw new RuntimeException(e);
}
}
private void removeExitCommand() {
commands.remove("exit");
}
@SuppressWarnings("unused")
@CallDescription(text = "Quits the program.")
private void exit() {
this.exit = true;
}
public void execute() {
Scanner in = new Scanner(System.in);
do {
System.out.print("> ");
String input = in.nextLine();
if (input.trim().isEmpty()) continue;
Command command = parseCommand(input);
if (command == null) {
System.out.println("Unknown command '" + parseCommandName(input) + "'.");
} else {
try {
if (!command.execute(stripCommandName(input)))
System.out.println("Invalid arguments for '" + parseCommandName(input) + "'.");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
} while (!stopRunning());
}
private boolean stopRunning() {
switch (mode) {
case RUN_ONCE:
return true;
case RUN_UNTIL_EXIT:
return exit;
case ASK_TO_CONTINUE:
return !askToContinue();
default:
throw new RuntimeException("ExecutionMode unknown.");
}
}
private boolean askToContinue() {
System.out.print("Continue? [Y/n]: ");
Scanner in = new Scanner(System.in);
String input = in.nextLine();
if (input.toLowerCase().startsWith("n")) { return false; }
return true;
}
private String parseCommandName(String input) {
int firstSpace = input.indexOf(' ');
if (firstSpace == -1) { return input.substring(0); }
return input.substring(0, firstSpace);
}
private String stripCommandName(String input) {
int firstSpace = input.indexOf(' ');
if (firstSpace == -1) return "";
return input.substring(input.indexOf(' ')).trim();
}
private Command parseCommand(String input) {
return commands.get(parseCommandName(input));
}
@Retention(RetentionPolicy.RUNTIME)
public static @interface CallDescription {
String text();
}
@Retention(RetentionPolicy.RUNTIME)
public static @interface ParameterDescription {
String name();
String text() default "";
}
private static class Command {
private Object object;
private List<Method> methods;
public Command(Object object, Method[] methods) {
this.object = object;
this.methods = new ArrayList<Method>(Arrays.asList(methods));
// sort by descending number of arguments
Collections.sort(this.methods, new Comparator<Method>() {
@Override
public int compare(Method o1, Method o2) {
return o2.getParameterTypes().length - o1.getParameterTypes().length;
}
});
}
public boolean execute(String input) {
input = input.trim();
int n = (input.isEmpty()) ? 0 : input.split(" ").length;
for (Method m : methods) {
if (numParams(m) > n) continue;
if (numParams(m) < n) break;
Object[] parameters = ParameterMatcher.match(input, m.getParameterTypes());
if (parameters != null) {
Object result = null;
try {
m.setAccessible(true);
result = m.invoke(getObject(), parameters);
} catch (IllegalArgumentException e) {
// rethrow
throw e;
} catch (IllegalAccessException e) {
// should never happen
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
// if it's caused by an IllegalArgumentException, rethrow IllegalArgumentException
if (e.getCause() != null && e.getCause() instanceof IllegalArgumentException)
throw (IllegalArgumentException) e.getCause();
}
if (m.getReturnType() != void.class) System.out.println(result);
return true;
}
}
return false;
}
public Object getObject() {
return object;
}
public List<Method> getMethods() {
return methods;
}
public int getMaxNumberOfParams() {
return numParams(methods.get(0));
}
private int numParams(Method m) {
return m.getParameterTypes().length;
}
}
}