Вступление
Привет, хабраюзеры! Решил поведать вам о мини-библиотеке Jerminal. Я сейчас работаю над большим коммерческим проектом на Groovy/Java. Ну и мне пришло задание — написать консольку для приложения. К сожалению, было поставлено условие: никаких сторонних решений, все только свое. Недолго думая, я сел и написал ее. Подробнее — под катом.
Цели
В принципе, чего-то очень серьезного от меня не требовали. Вот, собственно, параметры:
- Должна быть консолька, принимающая команды с текстовыми аргументами (без заморочек — просто строковый массив).
- Должно быть приглашение которое вводу, которое можно будет менять в коде.
- Можно привязывать методы к командам с помощью Reflection и лямбда-выражений.
В целом довольно простой список. Тем, кому не терпится посмотреть на результат, ссылку на репозиторий даю: kkremen/jerminal.
Обзор кода
Ну а теперь к делу. Немного почитав про Reflection и лямбды, я решил сделать "ход конем".
В первую очередь я создал интерфейс Executable
:
package org.meinkopf.console;
import java.lang.reflect.InvocationTargetException;
public interface Executable {
Object invoke(Object[] args) throws InvocationTargetException, IllegalAccessException;
}
Если кто не знает, то можно сделать так:
Executable ex = (args) -> {
return null;
}
ex.invoke(someArgs);
Теперь при вызове ex.invoke(...)
выполнится лямбда-выражение. Ну а для поддержки Reflection я создал класс BasicExecutable
, наследующий Executable
:
package org.meinkopf.console;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
@SuppressWarnings({ "WeakerAccess", "unused" })
public class BasicExecutable implements Executable {
protected Method method;
protected Object target;
public BasicExecutable(Method method, Object target) {
this.method = method;
this.target = target;
}
/* Getters and Setters */
public Object invoke(Object[] args) throws InvocationTargetException, IllegalAccessException {
if (args.length < method.getParameterCount()) {
return "Too few arguments!\n"; // если аргументов слишком мало, выводим на консоль предупреждение
}
return method.invoke(target, Arrays.copyOfRange(args, 0, method.getParameterCount())); // если аргументов слишком много, просто обрезаем ненужные
}
}
Еще я написал пару интерфейс-класс для списка команд: CommandList
и BasicCommandList
соответственно. Базовый класс хранит команды в Map < String, Executable >
и возвращает основному классу объекты типа Executable
.
package org.meinkopf.console;
public interface CommandList {
Executable get(String commandName);
}
package org.meinkopf.console;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings({ "unused", "WeakerAccess" })
public class BasicCommandList implements CommandList {
protected Map < String, Executable > methodMap = new HashMap <>();
@Override
public Executable get(String command) {
return methodMap.get(command);
}
public void register(@SuppressWarnings("SameParameterValue") String name, Executable command) {
methodMap.put(name, command);
}
}
Служебный класс Command
хранит разобранную парсером команду в виде строки-имени и ArrayList
аргументов. Аргументы, кстати, принимаются только строковые, то есть числа тоже передаются в виде строки.
Основной класс реализует Runnable
, но пока я это никак не использовал, и вызываю метод run
напрямую.
package org.meinkopf.console;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
@SuppressWarnings({ "WeakerAccess", "unused", "SpellCheckingInspection" })
public class Jerminal implements Runnable {
public static String PROMPT = " ~ $ ";
public static String HEADER = "Jerminal v0.0.1 for JVM Apps\nCopyright: Karl Meinkopf, 2017\n";
protected ArrayList < Command > commandHistory = new ArrayList <>();
protected CommandList commandList = null;
private Scanner scanner;
public JConsole(CommandList list) {
commandList = list;
scanner = new Scanner(System.in);
}
protected Command parse(String rawCommand) {
rawCommand = rawCommand.replaceAll("(\"[\\s\\S]*?\"|[\\S]+)\\s*", "$1\u0001");
rawCommand = rawCommand.replaceAll("\"([\\s\\S]*?)\"", "$1");
String[] rawList = rawCommand.split("\u0001");
ArrayList < String > args = new ArrayList <>(Arrays.asList(rawList));
String command = args.remove(0).trim();
return new Command(command, rawCommand, args);
}
protected Object execute(Command command) throws InvocationTargetException, IllegalAccessException {
Executable executable = commandList.get(command.getName());
if (executable == null) {
return "Can't find command: '" + command.getName() + "'";
}
return executable.invoke(command.getArgs().toArray());
}
protected void prompt( ) {
System.err.println();
System.err.print(PROMPT);
Command command = parse(getInputLine());
Object result;
try {
result = execute(command);
} catch (InvocationTargetException | IllegalAccessException e) {
System.err.println("Can not execute command '" + command.getName() + "'!\n\tReason: " + e.getMessage());
return;
}
if (result != null)
System.err.println(result.toString());
}
protected String getInputLine( ) {
return scanner.nextLine();
}
public void run( ) {
System.err.println(HEADER);
//noinspection InfiniteLoopStatement
while (true) {
prompt();
}
}
}
Заключение
Ну вот и вся библиотека. Пока маленькая, но я планирую ее развивать и улучшать. Если есть замечания и советы — буду рад услышать.