Jerminal — эмулятор терминала для Java-программ +9
Вступление
Привет, хабраюзеры! Решил поведать вам о мини-библиотеке 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 = "JConsole v0.0.0 for Java Apps\nAuthor: Karl Meinkopf\nBuilt especially for Protium project\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();
}
}
}
Заключение
Ну вот и вся библиотека. Пока маленькая, но я планирую ее развивать и улучшать. Если есть замечания и советы — буду рад услышать.
Комментарии (12)
AndreyRubankov
24.04.2017 15:17+3А вас не смущает, что в пакете поставки JDK уже есть утилита JConsole, которая выполняет совершенно другую роль? Перед прочтением статьи, было мнение, что статья будет именно про эту утилиту.
KarlKremen
24.04.2017 18:54Спасибо за информацию, исправлю!
AndreyRubankov
25.04.2017 08:14Посмотрел код, думаю, Вам будет полезно почитать вот такой туториал по SPI:
https://docs.oracle.com/javase/tutorial/ext/basics/spi.html «Система плагинов» из коробки =)
Сама консоль — это будет ядро с какой-то функцией-заглушкой, которая будет вызываться, если команда не была найдена. А все остальные команды будут подкладываться к приложению отдельным jar, в котором будет реализация SPI команд-хендлера.
Кода будет меньше, код будет чище, плагины и модульность проекта из коробки.
sshikov
24.04.2017 19:45К сожалению, было поставлено условие: никаких сторонних решений, все только свое.
Никогда не понимал подобных условий.
lxsmkv
24.04.2017 19:47А контекстный диалог она поддерживает? Например: «создай», «что создать?» «игрок» «имя?» «Иван» «игрок Иван создан». Как я из кода понял что нет. Т.е параметры команды надо знать заранее. А вообще насколько сложнее сделать поддержку контекстного диалога? Может кто-нибудь может привести пример удачной реализации?
KarlKremen
24.04.2017 19:54В данном случае это лишь вопрос реализации Executable. То есть, теоретически это сделать можно как-нибудь вот так:
Executable exec = (args) -> { System.out.println("Prompt: "); String command = readLine(); // будем предполагать, что такая функция реализована. ... }
Я подумаю, как добавить поддержку таких комманд "из коробки".
lxsmkv
24.04.2017 21:54мне представлялось это как-то так: каждая команда имеет диалоговые варианты, если пользователь дает один из приемлимых вариантов, обработка передается следующей команде, и так пока цепочка команд не будет выполнена. Если мы не даем команде подходящего уточнения, ввод зацикливается на команду, пока мы не дадим приемлимый параметр, либо не дадим отбой. Но все это в какие то синтаксические деревья уходит если вовремя не остановится. Так что я вообще не уверен в целесообразности такого подхода.
interprise
для начала не плохо бы на гитхаб залить
KarlKremen
Репозиторий проекта есть.