diff --git a/cliche-shell-core/pom.xml b/cliche-shell-core/pom.xml index 580bb52..ef72625 100644 --- a/cliche-shell-core/pom.xml +++ b/cliche-shell-core/pom.xml @@ -35,7 +35,10 @@ commons-beanutils-core 1.8.3 - + + com.google.code.findbugs + jsr305 + diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/CommandTable.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/CommandTable.java index aab72e1..fa136bc 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/CommandTable.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/CommandTable.java @@ -57,7 +57,6 @@ public List getCommandsNames(String prefix) { public void addMethod(Method method, Object handler, String prefix) { Command annotation = method.getAnnotation(Command.class); - assert method != null; String name; String autoAbbrev = null; diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/ConsoleIO.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/ConsoleIO.java index a16b059..ff8d6af 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/ConsoleIO.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/ConsoleIO.java @@ -1,270 +1,42 @@ -/* - * This file is part of the Cliche project, licensed under MIT License. - * See LICENSE.txt file in root folder of Cliche sources. - */ - package com.maxifier.cliche; -import com.maxifier.cliche.util.Strings; - -import jline.console.ConsoleReader; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import javax.annotation.Nullable; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintStream; -import java.lang.reflect.Array; -import java.util.Collection; import java.util.List; /** - * Console IO subsystem. - * This is also one of special command handlers and is responsible - * for logging (duplicating output) and execution of scripts. + * ConsoleIO * - * @author ASG + * @author aleksey.didik@maxifier.com (Aleksey Didik) (2013-07-17 13:32) */ -public class ConsoleIO implements Input, Output, ShellManageable { - - public ConsoleIO(ConsoleReader in, PrintStream out, PrintStream err) { - this.in = in; - this.out = out; - this.err = err; - } - - public ConsoleIO() throws IOException { - this(new ConsoleReader(System.in, System.out), - System.out, System.err); - } - - private ConsoleReader in; - private PrintStream out; - private PrintStream err; - - private int lastCommandOffset = 0; - - public String readCommand(List path) { - try { - String prompt = Strings.joinStrings(path, false, '/'); - switch (inputState) { - case USER: - return readUsersCommand(prompt); - case SCRIPT: - String command = readCommandFromScript(prompt); - if (command != null) { - return command; - } else { - closeScript(); - return readUsersCommand(prompt); - } - } - return readUsersCommand(prompt); - } catch (IOException ex) { - throw new Error(ex); - } - } - - private static final String USER_PROMPT_SUFFIX = "> "; - private static final String FILE_PROMPT_SUFFIX = "$ "; - - private static enum InputState { USER, SCRIPT } - - private InputState inputState = InputState.USER; - - private String readUsersCommand(String prompt) throws IOException { - String completePrompt = prompt+ USER_PROMPT_SUFFIX; - //print(completePrompt); - lastCommandOffset = completePrompt.length(); - - String command = in.readLine(completePrompt); - if (log != null) { - log.println(command); - } - return command; - } - - private BufferedReader scriptReader = null; - - private String readCommandFromScript(String prompt) throws IOException { - String command = scriptReader.readLine(); - if (command != null) { - String completePrompt = prompt+ FILE_PROMPT_SUFFIX; - print(completePrompt); - lastCommandOffset = completePrompt.length(); - } - return command; - } - - private void closeScript() throws IOException { - if (scriptReader != null) { - scriptReader.close(); - scriptReader = null; - } - inputState = InputState.USER; - } - - @Command(description="Reads commands from file") - public void runScript( - @Param(name="filename", description="Full file name of the script") - String filename - ) throws FileNotFoundException { - - scriptReader = new BufferedReader(new InputStreamReader(new FileInputStream(filename))); - inputState = InputState.SCRIPT; - } - - - public void outputHeader(String text) { - if (text != null) { - println(text); - } - } - - public void output(Object obj, OutputConversionEngine oce) { - if (obj == null) { - return; - } else { - obj = oce.convertOutput(obj); - } - - if (obj.getClass().isArray()) { - int length = Array.getLength(obj); - for (int i = 0; i < length; i++) { - output(Array.get(obj, i), 0, oce); - } - } else if (obj instanceof Collection) { - for (Object elem : (Collection)obj) { - output(elem, 0, oce); - } - } else { - output(obj, 0, oce); - } - } - - private void output(Object obj, int indent, OutputConversionEngine oce) { - if (obj == null) { - return; - } - - if (obj != null) { - obj = oce.convertOutput(obj); - } - - for (int i = 0; i < indent; i++) { - print("\t"); - } - - if (obj == null) { - println("(null)"); - } else if (obj.getClass().isPrimitive() || obj instanceof String) { - println(obj); - } else if (obj.getClass().isArray()) { - println("Array"); - int length = Array.getLength(obj); - for (int i = 0; i < length; i++) { - output(Array.get(obj, i), indent + 1, oce); - } - } else if (obj instanceof Collection) { - println("Collection"); - for (Object elem : (Collection)obj) { - output(elem, indent + 1, oce); - } - } else if (obj instanceof Throwable) { - println(obj); // class and its message - ((Throwable)obj).printStackTrace(out); - } else { - println(obj); - } - } - - private void print(Object x) { - out.print(x); - if (log != null) { - log.print(x); - } - } - - private void println(Object x) { - out.println(x); - if (log != null) { - log.println(x); - } - } - - private void printErr(Object x) { - err.print(x); - if (log != null) { - log.print(x); - } - } - - private void printlnErr(Object x) { - err.println(x); - if (log != null) { - log.println(x); - } - } - - public void outputException(String input, TokenException error) { - int errIndex = error.getToken().getIndex() + lastCommandOffset; - while (errIndex-- > 0) { - printErr("-"); - } - for (int i = 0; i < error.getToken().getString().length(); i++) { - printErr("^"); - } - printlnErr(""); - printlnErr(error); - } - - public void outputException(Throwable e) { - printlnErr(e); - if (e.getCause() != null) { - printlnErr(e.getCause()); - } - } - - private PrintStream log = null; - - private boolean isLoggingEnabled() { - return log != null; - } - - private int loopCounter = 0; - - public void cliEnterLoop() { - if (isLoggingEnabled()) { - loopCounter++; - } - } - - public void cliLeaveLoop() { - if (isLoggingEnabled()) { - loopCounter--; - } - if (loopCounter < 0) { - disableLogging(); - } - } - - @Command(description="Sets up logging, which duplicates all subsequent output in a file") - public void enableLogging( - @Param(name="fileName", description="Name of the logfile") String filename - ) throws FileNotFoundException { - - log = new PrintStream(filename); - loopCounter = 0; - } - - @Command(description="Turns off logging") - public String disableLogging() { - if (log != null) { - log.close(); - log = null; - return "Logging disabled"; - } else return "Logging is already disabled"; - } - +public interface ConsoleIO { + + /** + * Read command string with suggested prompt. + * Command is trimmed and have no side spaces. + * + * @param prompt prompt message should be shown before cursor + * @return trimmed string command representation + */ + String readCommand(String prompt) throws IOException; + + /** + * Write command execution result to output + * + * @param commandResult command result + */ + void output(String commandResult); + + /** + * Output command syntax exception + * @param input command asked to be executed + * @param error {@see TokenException} with syntax error explanation + */ + void outputException(String input, TokenException error); + + /** + * Output other exception what is result of command execution + * @param e exception was thrown during command execution. + */ + void outputException(Throwable e); } diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/ConversionEngine.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/ConversionEngine.java new file mode 100644 index 0000000..0ac42c2 --- /dev/null +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/ConversionEngine.java @@ -0,0 +1,11 @@ +package com.maxifier.cliche; + +/** + * ConversionEngine + * + * @author aleksey.didik@maxifier.com (Aleksey Didik) (2013-07-17 16:28) + */ +public class ConversionEngine implements InputConversionEngine, OutputConversionEngine { + + +} diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/HelpCommandHandler.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/HelpCommandHandler.java index af76556..c29a61a 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/HelpCommandHandler.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/HelpCommandHandler.java @@ -47,87 +47,10 @@ public List list() { } return result; } - - @Command(description="Generates an HTML file with command descriptions.\n" + - "(Similar to output of ?list, but in HTML format).") - public String generateHTMLHelp( - @Param(name="file-name", description="Path to the file to save the table to.") - String fileName, - @Param(name="include-prefixed", description="Whether to include commands with prefix " + - "(usually system or advanced functionality).") - boolean includePrefixed) throws IOException { - - final String HTML_FORMAT = "Auto-generated command reference file" + - "\n" + - "

%1$s Command Reference

\n" + - "Auto-generated by the Cliche Shell\n" + - "%2$s"; - - List commands = owner.getCommandTable().getCommandTable(); - StringBuilder commandsHTML = new StringBuilder(); - for (ShellCommand command : commands) { - if (command.getPrefix().equals("")) { - appendCommandHTML(commandsHTML, command); - } - } - if (includePrefixed) { - for (ShellCommand command : commands) { - if (!command.getPrefix().equals("")) { - appendCommandHTML(commandsHTML, command); - } - } - } - - String html = String.format(HTML_FORMAT, htmlEncode(owner.getAppName()), commandsHTML); - - File file = new File(fileName); - OutputStreamWriter w = new FileWriter(file); - try { - w.write(html); - } finally { - w.close(); - } - return String.format("Command table saved to %s", file.getAbsolutePath()); - } - - private static void appendCommandHTML(StringBuilder commandsHTML, ShellCommand command) { - - final String COMMAND_FORMAT = "

%2$s %3$s

\n" + - "

abbrev: %1$s

\n" + - "

%4$s

\n" + - "\n" + - "\n" + - "%5$s" + - "
parametertypedescription
\n"; - final String PARAM_FORMAT = "%1$s%2$s%3$s\n"; - - StringBuilder paramsHTML = new StringBuilder(); - ShellCommandParamSpec[] paramSpecs = command.getParamSpecs(); - for (ShellCommandParamSpec ps : paramSpecs) { - paramsHTML.append(String.format(PARAM_FORMAT, - htmlEncode(ps.getName()), - htmlEncode(ps.getValueClass().getSimpleName()), - htmlEncode(ps.getDescription()))); - } - - commandsHTML.append(String.format(COMMAND_FORMAT, - htmlEncode(command.getPrefix() + command.getAbbreviation()), - htmlEncode(command.getPrefix() + command.getName()), - htmlEncode(formatCommandParamsShort(command)), - htmlEncode(command.getDescription()), - paramsHTML)); - } - private static String htmlEncode(String s) { - return s; // for now it's app developer's responsibility to ensure html-compatibility of the strings. - // Quick and dirty. But there's no htmlEncode in the JDK, - // and Jakarta Commons is no good in case of Cliche: there be no dependendencies! - } - @Command(description="List all available commands starting with given string", header=COMMAND_LIST_HEADER) - public List list( - @Param(name="startsWith", description="Pattern to show commands starting with") String startsWith) { + public List list(@Param(name="startsWith", description="Pattern to show commands starting with") String startsWith) { List commands = owner.getCommandTable().getCommandTable(); List result = new ArrayList(commands.size()); @@ -139,15 +62,6 @@ public List list( return result; } - @Command(description="Show info on using the UI") - public Object help() { - return - "This is Cliche shell (" + Shell.PROJECT_HOMEPAGE_URL + ").\r\n" + - "To list all available commands enter ?list or ?list-all, " + - "the latter will also show you system commands. To get detailed info " + - "on a command enter ?help command-name"; - } - @Command(description="Show detailed info on all commands with given name") public Object help( @Param(name="command-name", description="Command name you want help on") String commandName) { diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionEngine.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionEngine.java index 88d1faa..1ac2b57 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionEngine.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionEngine.java @@ -21,7 +21,7 @@ * * @author ASG */ -public class InputConversionEngine { +public interface InputConversionEngine { private List inputConverters = new ArrayList(); @@ -120,7 +120,7 @@ private static Object convertArgToElementaryType(String string, Class aClass) th return c.newInstance(string); } catch (Exception ex) { throw new CLIException(String.format( - "Error instantiating class %c using string %s", aClass, string), ex); + "Error instantiating class %s using string %s", aClass.getName(), string), ex); } } catch (NoSuchMethodException e) { throw new CLIException("Can't convert string to " + aClass.getName()); @@ -128,25 +128,4 @@ private static Object convertArgToElementaryType(String string, Class aClass) th } } - public void addDeclaredConverters(Object handler) { - Field[] fields = handler.getClass().getFields(); - final String PREFIX = "CLI_INPUT_CONVERTERS"; - for (Field field : fields) { - if (field.getName().startsWith(PREFIX) - && field.getType().isArray() - && InputConverter.class.isAssignableFrom(field.getType().getComponentType())) { - try { - Object convertersArray = field.get(handler); - for (int i = 0; i < Array.getLength(convertersArray); i++) { - addConverter((InputConverter)Array.get(convertersArray, i)); - } - } catch (Exception ex) { - throw new RuntimeException("Error getting converter from field " + field.getName(), ex); - } - } - } - } - - - } diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionException.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionException.java new file mode 100644 index 0000000..d8a06d5 --- /dev/null +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConversionException.java @@ -0,0 +1,17 @@ +package com.maxifier.cliche; + +/** + * InputConversionException + * + * @author aleksey.didik@maxifier.com (Aleksey Didik) (2013-07-17 16:20) + */ +public class InputConversionException extends Exception { + + public InputConversionException(String message) { + super(message); + } + + public InputConversionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConverter.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConverter.java index 3387aff..24d1ef4 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConverter.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/InputConverter.java @@ -5,21 +5,8 @@ package com.maxifier.cliche; -/** - * This interface is used by the Shell to support new argument types. - * It converts string to an object of given class. - * @author ASG - */ public interface InputConverter { - /** - * String-to-someClass conversion method - * May throw any exception if string is considered invalid for given class; - * must do nothing but return null if doesn't recognize the toClass. - * @param original String to be converted - * @param toClass Class to be converted to - * @return Object of the class toClass or null, if don't know how to convert to given class - * - * @see asg.cliche.Shell - */ - Object convertInput(String original, Class toClass) throws Exception; + + Object convertInput(String commandParameter, + InputConversionEngine inputConversionEngine) throws InputConversionException; } diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/JLineConsoleIO.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/JLineConsoleIO.java new file mode 100644 index 0000000..bb2a58b --- /dev/null +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/JLineConsoleIO.java @@ -0,0 +1,66 @@ +/* + * This file is part of the Cliche project, licensed under MIT License. + * See LICENSE.txt file in root folder of Cliche sources. + */ + +package com.maxifier.cliche; + +import jline.console.ConsoleReader; + +import java.io.IOException; +import java.io.PrintStream; + +/** + * Console IO subsystem. + * This is also one of special command handlers and is responsible + * for logging (duplicating output) and execution of scripts. + * + * @author ASG + */ +public class JLineConsoleIO implements ConsoleIO { + + private ConsoleReader in; + private PrintStream out; + private PrintStream err; + + public JLineConsoleIO(ConsoleReader in, + PrintStream out, + PrintStream err) { + this.in = in; + this.out = out; + this.err = err; + } + + @Override + public String readCommand(String prompt) throws IOException { + return in.readLine(String.format("%s> ", prompt)); + } + + @Override + public void output(String commandResult) { + out.println(commandResult); + out.flush(); + } + + @Override + public void outputException(String input, TokenException error) { + int errIndex = error.getToken().getIndex(); + while (errIndex-- > 0) { + err.print("-"); + } + for (int i = 0; i < error.getToken().getString().length(); i++) { + err.print("^"); + } + err.println(error.getMessage()); + } + + @Override + public void outputException(Throwable e) { + err.println(e); + if (e.getCause() != null) { + err.println(e.getCause()); + } + } + + +} diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConversionEngine.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConversionEngine.java index 8dbc877..8e0042d 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConversionEngine.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConversionEngine.java @@ -21,7 +21,7 @@ * * @author ASG */ -public class OutputConversionEngine { +public interface OutputConversionEngine { private List outputConverters = new ArrayList(); @@ -47,25 +47,4 @@ public Object convertOutput(Object anObject) { } return convertedOutput; } - - public void addDeclaredConverters(Object handler) { - Field[] fields = handler.getClass().getFields(); - final String PREFIX = "CLI_OUTPUT_CONVERTERS"; - for (Field field : fields) { - if (field.getName().startsWith(PREFIX) - && field.getType().isArray() - && OutputConverter.class.isAssignableFrom(field.getType().getComponentType())) { - try { - Object convertersArray = field.get(handler); - for (int i = 0; i < Array.getLength(convertersArray); i++) { - addConverter((OutputConverter)Array.get(convertersArray, i)); - } - } catch (Exception ex) { - throw new RuntimeException("Error getting converter from field " + field.getName(), ex); - } - } - } - } - - } diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConverter.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConverter.java index d2ea46a..6f2afee 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConverter.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/OutputConverter.java @@ -5,19 +5,9 @@ package com.maxifier.cliche; -/** - * This interface is used by the Shell to support new return types. - * It converts objects to other objects (usually strings) that will be displayed. - * @author ASG - */ +import java.lang.reflect.Method; + public interface OutputConverter { - /** - * Object-to--user-friendly-object (usually string) conversion method. - * The method must check argument's class, since it will be fed virtually all - * returned objects. Simply return null when not sure. - * @param toBeFormatted Object to be displayed to the user - * @return Object representing the object or Null if don't know how to make it. - * Do not return default toString() !! - */ - Object convertOutput(Object toBeFormatted); + + Object convertOutput(Object toBeFormatted, Method commandMethod); } diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/Shell.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/Shell.java index cefc059..d8c8e60 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/Shell.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/Shell.java @@ -12,322 +12,171 @@ package com.maxifier.cliche; -import com.maxifier.cliche.util.ArrayHashMultiMap; -import com.maxifier.cliche.util.MultiMap; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import javax.annotation.Nullable; import java.io.IOException; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; +import java.util.*; /** * Shell is the class interacting with user. - * Provides the command loop. - * All logic lies here. + * Provides the command loop or single command execution * * @author ASG + * @author aleksey.didik@maxifier.com (Aleksey Didik) */ public class Shell { + /** + * Used to create subsequence of commands + */ + public static final String COMMAND_UNION_SIGN = "&&"; + private final ConsoleIO consoleIO; + private final String prompt; - public static String PROJECT_HOMEPAGE_URL = "http://cliche.sourceforge.net"; + private Multimap commands = LinkedListMultimap.create(); - private Output output; - private Input input; - private String appName; + private final InputConversionEngine inputConversionEngine; + private final OutputConversionEngine outputConversionEngine; - public static class Settings { - private final Input input; - private final Output output; - private final MultiMap auxHandlers; - private final boolean displayTime; - public Settings(Input input, Output output, MultiMap auxHandlers, boolean displayTime) { - this.input = input; - this.output = output; - this.auxHandlers = auxHandlers; - this.displayTime = displayTime; - } - public Settings createWithAddedAuxHandlers(MultiMap addAuxHandlers) { - MultiMap allAuxHandlers = new ArrayHashMultiMap(auxHandlers); - allAuxHandlers.putAll(addAuxHandlers); - return new Settings(input, output, allAuxHandlers, displayTime); - } + private Multimap, InputConverter> inputConverters = LinkedListMultimap.create(); + private Multimap, OutputConverter> outputConverters = LinkedListMultimap.create(); - } + public Shell(ConsoleIO consoleIO, String prompt) { + this.consoleIO = consoleIO; + this.prompt = prompt; - public Settings getSettings() { - return new Settings(input, output, auxHandlers, displayTime); - } + ConversionEngine conversionEngine = new ConversionEngine(); + inputConversionEngine = conversionEngine; + outputConversionEngine = conversionEngine; + //add default input and output converters - public void setSettings(Settings s) { - input = s.input; - output = s.output; - displayTime = s.displayTime; - for (String prefix : s.auxHandlers.keySet()) { - for (Object handler : s.auxHandlers.get(prefix)) { - addAuxHandler(handler, prefix); - } - } - } - /** - * Shell's constructor - * You probably don't need this one, see methods of the ShellFactory. - * - * @param s Settings object for the shell instance - * @param commandTable CommandTable to store commands - * @param path Shell's location: list of path elements. - * @see asg.cliche.ShellFactory - */ - public Shell(Settings s, CommandTable commandTable, List path) { - this.commandTable = commandTable; - this.path = path; - setSettings(s); + //add built-in commands } - private CommandTable commandTable; - /** - * @return the CommandTable for this shell. + * Add new command handler. + * @param handler object with methods annotated with @Commands */ - public CommandTable getCommandTable() { - return commandTable; + public void addHandler(Object handler) { + addHandler(handler, null); } - private OutputConversionEngine outputConverter = new OutputConversionEngine(); - /** - * Call this method to get OutputConversionEngine used by the Shell. + * Add new command handler and all methods in this handler will be prefixed with provided prefix. + *
+ * For example, instance of this class: + *
+     *     class Foo {
      *
-     * @return a conversion engine.
-     */
-    public OutputConversionEngine getOutputConverter() {
-        return outputConverter;
-    }
-
-    private InputConversionEngine inputConverter = new InputConversionEngine();
-
-    /**
-     * Call this method to get InputConversionEngine used by the Shell.
+     *        {@literal @}Command(name="hello")
+     *         public String command() {
+     *             return "Hello world!"
+     *         }
+     *     }
      *
-     * @return a conversion engine.
-     */
-    public InputConversionEngine getInputConverter() {
-        return inputConverter;
-    }
-
-    private MultiMap auxHandlers = new ArrayHashMultiMap();
-    private List allHandlers = new ArrayList();
-
-
-    /**
-     * Method for registering command hanlers (or providers?)
-     * You call it, and from then the Shell has all commands declare in
-     * the handler object.
-     * 

- * This method recognizes if it is passed ShellDependent or ShellManageable - * and calls corresponding methods, as described in those interfaces. + * shell.addHandler(new Foo(), "!"); + * * - * @param handler Object which should be registered as handler. - * @param prefix Prefix that should be prepended to all handler's command names. - * @see asg.cliche.ShellDependent - * @see asg.cliche.ShellManageable - */ - public void addMainHandler(Object handler, String prefix) { - if (handler == null) { - throw new NullPointerException(); - } - allHandlers.add(handler); - - addDeclaredMethods(handler, prefix); - inputConverter.addDeclaredConverters(handler); - outputConverter.addDeclaredConverters(handler); - - if (handler instanceof ShellDependent) { - ((ShellDependent) handler).cliSetShell(this); - } - } - - /** - * This method is very similar to addMainHandler, except ShellFactory - * will pass all handlers registered with this method to all this shell's subshells. + * Will be translated to command '!hello' + *
+ * Mostly usable to specific commands set like help and so on. * - * @param handler Object which should be registered as handler. - * @param prefix Prefix that should be prepended to all handler's command names. - * @see asg.cliche.Shell#addMainHandler(java.lang.Object, java.lang.String) + * @param handler object with methods annotated with @Commands + * @param prefix prefix would be used for any command name from this handler */ - public void addAuxHandler(Object handler, String prefix) { - if (handler == null) { - throw new NullPointerException(); - } - auxHandlers.put(prefix, handler); - allHandlers.add(handler); + public void addHandler(Object handler, @Nullable String prefix) { + Preconditions.checkNotNull(handler, "Handler instance can't be null"); + //TODO - addDeclaredMethods(handler, prefix); - inputConverter.addDeclaredConverters(handler); - outputConverter.addDeclaredConverters(handler); - - if (handler instanceof ShellDependent) { - ((ShellDependent) handler).cliSetShell(this); - } } - private void addDeclaredMethods(Object handler, String prefix) throws SecurityException { - for (Method m : handler.getClass().getMethods()) { - Command annotation = m.getAnnotation(Command.class); - if (annotation != null) { - commandTable.addMethod(m, handler, prefix); - } - } + public void addInputConverter(Class classType, InputConverter inputConverter) { + inputConverters.put(classType, inputConverter); } - private Throwable lastException = null; - - /** - * Returns last thrown exception - */ - @Command(description = "Returns last thrown exception") // Shell is self-manageable, isn't it? - public Throwable getLastException() { - return lastException; + public void addOutputConverter(Class classType, OutputConverter outputConverter) { + outputConverters.put(classType, outputConverter); } - private List path; - /** - * @return list of path elements, as it was passed in constructor + * Process command line through ConsoleIO. + * @param commandLine command line line to be processed. */ - public List getPath() { - return path; - } - - /** - * Runs the command session. - * Create the Shell, then run this method to listen to the user, - * and the Shell will invoke Handler's methods. - * - * @throws java.io.IOException when can't readLine() from input. - */ - public void commandLoop() throws IOException { - for (Object handler : allHandlers) { - if (handler instanceof ShellManageable) { - ((ShellManageable) handler).cliEnterLoop(); - } - } - output.output(appName, outputConverter); - String command = ""; - while (!command.trim().equals("exit")) { - try { - command = input.readCommand(path); - if (!Strings.isNullOrEmpty(command)) { - processLine(command); - } - } catch (TokenException te) { - lastException = te; - output.outputException(command, te); - } catch (CLIException clie) { - lastException = clie; - if (!command.trim().equals("exit")) { - output.outputException(clie); - } - } - } - for (Object handler : allHandlers) { - if (handler instanceof ShellManageable) { - ((ShellManageable) handler).cliLeaveLoop(); - } + public void processCommand(String commandLine) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(commandLine), "Command should not be null"); + //parse commandLine + String[] commandsInLine = commandLine.split(COMMAND_UNION_SIGN); + for (String basicCommand : commandsInLine) { + processCommand0(basicCommand); } } - private void outputHeader(String header, Object[] parameters) { - if (header == null || header.isEmpty()) { - output.outputHeader(null); - } else { - output.outputHeader(String.format(header, parameters)); + @VisibleForTesting + void processCommand0(String command) { + List tokens = Token.tokenize(command); + ShellCommand shellCommand = lookupCommand(command); + Class[] parameterTypes = shellCommand.getMethod().getParameterTypes(); + Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + Class parameterType = parameterTypes[i]; + String parameterString = tokens.get(i).getString(); } - } - - private static final String HINT_FORMAT = "This is %1$s, running on Cliche Shell\n" + - "For more information on the Shell, enter ?help"; + for (Class parameterType : parameterTypes) { + synchronized (inputConverters) { - /** - * You can operate Shell linewise, without entering the command loop. - * All output is directed to shell's Output. - * - * @param line Full command line - * @throws asg.cliche.CLIException This may be TokenException - * @see asg.cliche.Output - */ - public void processLine(String line) throws CLIException { - if (line.trim().equals("?")) { - output.output(String.format(HINT_FORMAT, appName), outputConverter); - } else { - List tokens = Token.tokenize(line); - if (tokens.size() > 0) { - String discriminator = tokens.get(0).getString(); - processCommand(discriminator, tokens); } } - } - - private void processCommand(String discriminator, List tokens) throws CLIException { - assert discriminator != null; - assert !discriminator.equals(""); - - ShellCommand commandToInvoke = commandTable.lookupCommand(discriminator, tokens); + Class[] paramClasses = parameterTypes; + Object[] parameters = new Object[paramClasses.length]; - Class[] paramClasses = commandToInvoke.getMethod().getParameterTypes(); Object[] parameters = inputConverter.convertToParameters(tokens, paramClasses, commandToInvoke.getMethod().isVarArgs()); - - outputHeader(commandToInvoke.getHeader(), parameters); - - long timeBefore = Calendar.getInstance().getTimeInMillis(); - Object invocationResult = commandToInvoke.invoke(parameters); - long timeAfter = Calendar.getInstance().getTimeInMillis(); + Object result = commandToInvoke.invoke(parameters); if (invocationResult != null) { - output.output(invocationResult, outputConverter); - } - if (displayTime) { - final long time = timeAfter - timeBefore; - if (time != 0L) { - output.output(String.format(TIME_MS_FORMAT_STRING, time), outputConverter); - } + consoleIO.output(); } - } - - private static final String TIME_MS_FORMAT_STRING = "time: %d ms"; - private boolean displayTime = false; - - /** - * Turns command execution time display on and off - * - * @param displayTime true if do display, false otherwise - */ - @Command(description = "Turns command execution time display on and off") - public void setDisplayTime( - @Param(name = "do-display-time", description = "true if do display, false otherwise") - boolean displayTime) { - this.displayTime = displayTime; } + private ShellCommand lookupCommand(String command) { + List tokens = Token.tokenize(command); + String discriminator = tokens.get(0).getString(); + } /** - * Hint is some text displayed before the command loop and every time user enters "?". + * Runs the command session. + * Create the Shell, then run this method to listen to the user, + * and the Shell will invoke Handler's methods. + * + * @throws java.io.IOException when can't readLine() from input. */ - public void setAppName(String appName) { - this.appName = appName; - } - - public String getAppName() { - return appName; + public void commandLoop() throws IOException { + String command; + do { + command = consoleIO.readCommand(prompt); + if (!Strings.isNullOrEmpty(command)) { + List tokens = ; + if (tokens.size() > 0) { + String discriminator = tokens.get(0).getString(); + try { + processCommand(discriminator, tokens); + } catch (CLIException e) { + e.printStackTrace(); + } + } + } + } while(); } - } diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellCommandParamSpec.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellCommandParamSpec.java index 6f299fd..b4d9d0e 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellCommandParamSpec.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellCommandParamSpec.java @@ -24,7 +24,6 @@ static ShellCommandParamSpec[] forMethod(Method theMethod) { } } if (paramAnnotation != null) { - assert !paramAnnotation.name().isEmpty() : "@Param.name mustn\'t be empty"; result[i] = new ShellCommandParamSpec(paramAnnotation.name(), paramTypes[i], paramAnnotation.description(), i); } else { diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellFactory.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellFactory.java index 72ce398..611d9bd 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellFactory.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/ShellFactory.java @@ -45,7 +45,7 @@ private ShellFactory() { * @return Shell that can be either further customized or run directly by calling commandLoop(). */ public static Shell createConsoleShell(String prompt, String appName, Object... handlers) throws IOException { - ConsoleIO io = new ConsoleIO(); + ConsoleIO io = new JLineConsoleIO(); List path = new ArrayList(1); path.add(prompt); @@ -83,7 +83,7 @@ public void run() { try { PrintStream out = new PrintStream(socket.getOutputStream()); ConsoleReader consoleReader = new ConsoleReader(socket.getInputStream(), out); - ConsoleIO io = new ConsoleIO(consoleReader, out, out); + ConsoleIO io = new JLineConsoleIO(consoleReader, out, out); List path = new ArrayList(1); path.add(promt); @@ -160,7 +160,7 @@ public Command create() { */ public static Shell createConsoleShell(String prompt, String appName, Object mainHandler, MultiMap auxHandlers) throws IOException { - ConsoleIO io = new ConsoleIO(); + ConsoleIO io = new JLineConsoleIO(); List path = new ArrayList(1); path.add(prompt); diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/SshShellCommand.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/SshShellCommand.java index e036de7..d0a2b83 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/SshShellCommand.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/SshShellCommand.java @@ -27,7 +27,7 @@ /** * @author aleksey.didik@maxifier.com Aleksey Didik */ -class SshShellCommand implements Command { +public class SshShellCommand implements Command { private final ExecutorService executor; private final String promt; @@ -35,14 +35,15 @@ class SshShellCommand implements Command { private final Collection handlersCollection; private InputStream in; private PrintStream out; - private PrintStream err; private ExitCallback callback; private Future future; private ConsoleReader consoleReader; private Shell theShell; - public SshShellCommand(ExecutorService executor, String promt, String appName, Collection handlersCollection) { - //To change body of created methods use File | Settings | File Templates. + public SshShellCommand(ExecutorService executor, + String promt, + String appName, + Collection handlersCollection) { this.executor = executor; this.promt = promt; this.appName = appName; @@ -81,12 +82,11 @@ public void run() { consoleReader.setHistoryEnabled(true); consoleReader.setExpandEvents(false); - ConsoleIO io = new ConsoleIO(consoleReader, out, out); + ConsoleIO io = new JLineConsoleIO(consoleReader, out, out); List path = new ArrayList(1); path.add(promt); - theShell = new Shell(new Shell.Settings(io, io, new EmptyMultiMap(), false), + theShell = new Shell(new Shell.Settings(io, io, new EmptyMultiMap()), new CommandTable(new DashJoinedNamer(true)), path); - theShell.setAppName(appName); theShell.addMainHandler(theShell, "!"); theShell.addMainHandler(new HelpCommandHandler(), "?"); for (Object h : handlersCollection) { diff --git a/cliche-shell-core/src/main/java/com/maxifier/cliche/Token.java b/cliche-shell-core/src/main/java/com/maxifier/cliche/Token.java index 3fa0ab9..73319b6 100644 --- a/cliche-shell-core/src/main/java/com/maxifier/cliche/Token.java +++ b/cliche-shell-core/src/main/java/com/maxifier/cliche/Token.java @@ -73,9 +73,6 @@ public int hashCode() { * * @param input String to be tokenized * @return List of tokens - * - * @see asg.cliche.Shell.Token - * @see asg.cliche.Shell.escapeString */ /*package-private for tests*/ static List tokenize(final String input) { diff --git a/cliche-shell-guice/src/main/java/com/maxifier/cliche/guice/SshShellModule.java b/cliche-shell-guice/src/main/java/com/maxifier/cliche/guice/SshShellModule.java index 53b870a..d236873 100644 --- a/cliche-shell-guice/src/main/java/com/maxifier/cliche/guice/SshShellModule.java +++ b/cliche-shell-guice/src/main/java/com/maxifier/cliche/guice/SshShellModule.java @@ -1,13 +1,17 @@ package com.maxifier.cliche.guice; -import com.maxifier.cliche.ShellFactory; - import com.google.inject.AbstractModule; import com.google.inject.TypeLiteral; import com.google.inject.spi.InjectionListener; import com.google.inject.spi.TypeEncounter; import com.google.inject.spi.TypeListener; +import com.maxifier.cliche.ShellFactory; +import com.maxifier.cliche.SshShellCommand; import org.apache.sshd.SshServer; +import org.apache.sshd.common.Factory; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.session.ServerSession; @@ -15,6 +19,8 @@ import java.io.IOException; import java.security.PublicKey; import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * @author aleksey.didik@maxifier.com (Aleksey Didik) @@ -33,27 +39,41 @@ public SshShellModule(String promt, String appName, int port) { @Override protected void configure() { - SshServer sshServer = SshServer.setUpDefaultServer(); + final SshServer sshServer = SshServer.setUpDefaultServer(); + sshServer.setPublickeyAuthenticator(new PublickeyAuthenticator() { @Override public boolean authenticate(String username, PublicKey key, ServerSession session) { return true; } }); - // sshServer.setPasswordAuthenticator(new PasswordAuthenticator() { - // @Override - // public boolean authenticate(String username, String password, ServerSession session) { - // return true; //To change body of implemented methods use File | Settings | File Templates. - // } - // }); + sshServer.setPasswordAuthenticator(new PasswordAuthenticator() { + @Override + public boolean authenticate(String username, String password, ServerSession session) { + return true; + } + }); sshServer.setPort(port); sshServer.setKeyPairProvider(new SimpleGeneratorHostKeyProvider("hostkey.ser")); + + final Collection shellHandlers = ShellFactory.createSshShell(sshServer, promt, appName); + + sshServer.setCommandFactory(new CommandFactory() { + + @Override + public Command createCommand(String command) { + final ExecutorService executor = Executors.newCachedThreadPool(); + return new SshShellCommand(executor, promt, appName, shellHandlers); + } + }); + try { sshServer.start(); } catch (IOException e) { throw new RuntimeException(e); } + bindListener(AnnotatedClassMatcher.with(CommandHandler.class), new TypeListener() { @Override @@ -67,6 +87,7 @@ public void afterInjection(I injectee) { } } ); + } diff --git a/cliche-shell-guice/src/test/java/com/maxifier/cliche/guice/SshShellModuleTest.java b/cliche-shell-guice/src/test/java/com/maxifier/cliche/guice/SshShellModuleTest.java index 79fdeb6..f52a8b4 100644 --- a/cliche-shell-guice/src/test/java/com/maxifier/cliche/guice/SshShellModuleTest.java +++ b/cliche-shell-guice/src/test/java/com/maxifier/cliche/guice/SshShellModuleTest.java @@ -1,5 +1,8 @@ package com.maxifier.cliche.guice; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; import com.maxifier.cliche.Command; import com.brsanthu.dataexporter.model.AlignType; @@ -17,15 +20,15 @@ public class SshShellModuleTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println(new Foo().table()); -// Injector injector = Guice.createInjector( -// new SshShellModule("test", "Test App", 12891), -// new AbstractModule() { -// @Override -// protected void configure() { -// bind(Foo.class).asEagerSingleton(); -// } -// }); -// Thread.sleep(500000); + Injector injector = Guice.createInjector( + new SshShellModule("test", "Test App", 12891), + new AbstractModule() { + @Override + protected void configure() { + bind(Foo.class).asEagerSingleton(); + } + }); + Thread.sleep(500000); } diff --git a/pom.xml b/pom.xml index 85eb594..f1a4732 100644 --- a/pom.xml +++ b/pom.xml @@ -93,13 +93,17 @@ data-exporter 1.0.2 - - + + com.google.code.findbugs + jsr305 + 1.3.9 + - + + junit junit 4.10