As I was creating a CLI program, I realized having a ton of switch/if-then statements was a messy way to process user input. So, I set up a way to dynamically create commands as I added modules (Classes that extend the Module class) and methods. When a user enters a command like "sys::get bios" it gets passed to the loaded Module with the ID "sys". The base functionality defined in Module goes through each method in the runtime class and selects the best matching method. In this case, it would find get_bios()
and call it via reflection. I was wondering if anyone else has a way a improve this code, or expand its functionality.
Process Input Method:
public static void processInput(String input, boolean recordLast)
{
if (input.startsWith("|"))
{
if (input.length() > 1) {
List<String> output = Runner.runSimple("cmd.exe /c " + input.substring(1, input.length()));
output.forEach(l -> Logger.logVerbose(l));
}
}
else if (input.contains("::"))
{
//Command Syntax: MODULE::COMMAND ARGS
if (ModuleRegistry.getModule(input.split(Pattern.quote("::"))[0]) != null)
{
ModuleRegistry.getModule(input.split(Pattern.quote("::"))[0]).run(input.substring(input.indexOf("::")+2));
if (recordLast) lastInput = input;
}
else
{
Logger.logError("Invalid context: " + input.split(Pattern.quote("::"))[0]);
}
}
else if (input.equals("!"))
{
if (!lastInput.equals("!")) processInput(lastInput);
}
}
Module run method (Not overridden by a Module Class):
public void run(String input)
{
List<String> args = new ArrayList<String>();
Matcher m = argPattern.matcher(input);
while (m.find()) args.add(m.group(1).replace("\"", "")); //Get all args and remove any surrounding quotes
Method method = matchingMethod(args);
if (method == null) //No matching valid command methods
{
Logger.logVerbose("Command not found");
}
else
{
if (SystemType.isMatch(BlackStar.systemType, method.getAnnotation(ModuleCommand.class).supportedSystems()))
{
boolean isPrivate = !method.isAccessible();
try {
method.setAccessible(true); //Just incase the method is private, we need to make it accessible to call it
if (method.getParameterCount() > 0)
{
if (method.getParameterCount() == 1 && args.size() == 0) args.add(""); //If method expects 1 argument and we have none, add a blank string
method.invoke(this, (Object[]) args.toArray(new String[0]));
}
else
{
method.invoke(this, new Object[0]);
}
method.setAccessible(isPrivate); //Reset accessibility
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
Logger.logError("Invalid Arguments");
e.printStackTrace();
}
}
}
}
Method Matcher:
public Method matchingMethod(List<String> arguments)
{
//TODO Ignore methods that match the best match perfectly in case the super-class implementation is used instead of the overriden method.
Method bestMatch = null;
Class<?> superClass = this.getClass();
while (!(superClass == null) && !(superClass.equals(Object.class))) //Traverse class inheritance until top of tree is reached. Ignore Object class
{
for (Method method : superClass.getDeclaredMethods())
{
if (method.isAnnotationPresent(ModuleCommand.class)) //Is this a method that is allowed to be run as a command?
{
String[] mName = method.getName().split(Pattern.quote("_")); //Use the underscore as a delimiter in method name. Example "list_shadows" would match up to "list shadows"
if (mName[0].length() == 0) mName = ArrayUtil.subset(mName, 1, mName.length); //If method starts with "_" the first value is blank, and should be removed
boolean match = true;
for (int i = 0; i < Math.min(mName.length, arguments.size()); i++)
{
if (!mName[i].equals(arguments.get(i)))
{
match = false;
}
}
if (match)
{
if (bestMatch == null || bestMatch.getName().split(Pattern.quote("_")).length < method.getName().split(Pattern.quote("_")).length)
{
//The found method is either the first match, or more of the arguments match the method name
if (method.getName().split(Pattern.quote("_")).length + (method.getParameterCount()) == arguments.size()) {
bestMatch = method;
}
}
}
}
}
superClass = superClass.getSuperclass();
}
if (bestMatch != null) //If we have found any match
{
String[] mTokens = bestMatch.getName().split(Pattern.quote("_"));
if (mTokens[0].length() == 0) mTokens = ArrayUtil.subset(mTokens, 1, mTokens.length);
//Now remove any arguments that matched method name from the list
for (int i = 0; i < mTokens.length; i++) arguments.remove(0);
}
return bestMatch;
}
An example ModuleCommand in the Sys module:
@ModuleCommand(supportedSystems = SystemType.WINDOWS, friendlyName = "Get BIOS Info")
protected String get_bios()
{
String ret = "";
//Run command to get BIOS information
return ret;
}