/* * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.aitools.programd.interfaces.shell; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.Collection; import java.util.HashMap; import java.util.List; import org.aitools.programd.Bot; import org.aitools.programd.Bots; import org.aitools.programd.Core; import org.aitools.programd.predicates.PredicateManager; import org.aitools.programd.util.ManagedProcess; import org.aitools.util.runtime.Errors; import org.aitools.util.runtime.UserError; import org.aitools.util.xml.XHTML; import org.apache.log4j.Logger; import org.jdom.Document; import org.jdom.Element; import org.jdom.Namespace; /** * Provides a simple shell for interacting with the bot at a command line. * * @author <a href="mailto:noel@aitools.org">Noel Bush</a> * @author Jon Baer * @author Eion Robb */ public class Shell extends Thread { /** * An exception thrown if no command is specified. */ public class NoCommandException extends Exception { /** * */ private static final long serialVersionUID = 1L; // No body. } /** * An exception thrown if an invalid commandable is specified. */ public class NoSuchCommandableException extends Exception { /** * */ private static final long serialVersionUID = 1L; // No body. } /** The command registry. */ private ShellCommandRegistry commandRegistry; /** The Core to which this Shell is attached. */ private Core _core; /** The PredicateMaster in use by the attached Core. */ private PredicateManager predicateMaster; /** The Bots object in use by the attached Core. */ private Bots bots; /** A BufferedReader for user input to the shell. */ private BufferedReader inReader; /** Where regular console output will go. */ private PrintStream outStream; /** Where console errors will go. */ private PrintStream errStream; /** Where console prompt output will go. */ private PrintStream consolePrompt; /** A bot id. */ private String botid; /** A bot name. */ private String botName; /** The client name predicate. */ private String clientNamePredicate; /** The bot name predicate. */ private String botNamePredicate; /** The host name. */ private String hostname; /** An indicator used to keep track of whether we're midline in a console output (i.e., showing a prompt). */ private boolean midLine = false; private static final Namespace PLUGIN_CONFIG_NS = Namespace.getNamespace(Core.PLUGIN_CONFIG_NS_URI); /** * A <code>Shell</code> with default input and output streams ( <code>System.in</code> and <code>System.out</code>). * */ public Shell() { super("Shell"); this.inReader = new BufferedReader(new InputStreamReader(System.in)); this.outStream = this.errStream = this.consolePrompt = System.out; this.setDaemon(true); } /** * A <code>Shell</code> with custom input and output streams. * * @param in the input stream * @param out the output stream * @param err the error stream * @param prompt the prompt output stream */ public Shell(InputStream in, PrintStream out, PrintStream err, PrintStream prompt) { super("Shell"); this.inReader = new BufferedReader(new InputStreamReader(in)); this.outStream = out; this.errStream = err; this.consolePrompt = prompt; this.setDaemon(true); } /** * Attach this shell to the given core. * * @param core */ @SuppressWarnings("unchecked") public void attachTo(Core core) { this._core = core; this.botNamePredicate = this._core.getSettings().getBotNameProperty(); this.predicateMaster = this._core.getPredicateMaster(); this.bots = this._core.getBots(); this.clientNamePredicate = this._core.getSettings().getClientNamePredicate(); this.hostname = this._core.getHostname(); this.commandRegistry = new ShellCommandRegistry(); // Look for any shell command plugins and add them to the registry. Document plugins = this._core.getPluginConfig(); if (plugins != null) { Element shellCommandSet = plugins.getRootElement().getChild("shell-commands", PLUGIN_CONFIG_NS); if (shellCommandSet != null) { List<Element> commands = shellCommandSet.getChildren("command", PLUGIN_CONFIG_NS); if (commands != null) { for (Element commandElement : commands) { String classname = commandElement.getAttributeValue("class"); List<Element> parameterElements = commandElement.getChildren("parameter", PLUGIN_CONFIG_NS); if (parameterElements != null) { HashMap<String, String> parameters = new HashMap<String, String>(parameterElements.size()); for (Element parameter : parameterElements) { parameters.put(parameter.getAttributeValue("name"), parameter.getAttributeValue("value")); } this.commandRegistry.register(classname, parameters); } } } } } } /** * Sends a command to a shell commandable, if possible. * * @param command the command (including the shell commandable name) * @throws NoCommandException if no command is given * @throws NoSuchCommandableException if an invalid commandable is specified */ protected void commandCommandable(String command) throws NoCommandException, NoSuchCommandableException { // Parse out the commandable. int space = command.indexOf(' '); if (space == -1) { throw new Shell.NoCommandException(); } if (space == command.length()) { throw new Shell.NoCommandException(); } String commandableID = command.substring(1, space); ShellCommandable commandable = null; for (ManagedProcess process : this._core.getManagedProcesses().values()) { if (process instanceof ShellCommandable) { ShellCommandable candidate = (ShellCommandable) process; if (commandableID.equals(candidate.getShellID())) { commandable = candidate; } } } if (commandable == null) { throw new Shell.NoSuchCommandableException(); } commandable.processShellCommand(command.substring(space + 1)); } /** * @return the Bots object used by this shell */ public Bots getBots() { return this.bots; } /** * @return the command registry */ public Collection<ShellCommand> getCommands() { return this.commandRegistry.getValues(); } /** * @return the Core in use */ public Core getCore() { return this._core; } /** * @return the current bot id */ public String getCurrentBotID() { return this.botid; } /** * Tells the Shell that something else was printed to the console; not midLine anymore. */ public void gotLine() { this.midLine = false; } /** * Notes that the shell will not run, and sleeps. */ protected void noShell() { this._core.getLogger().warn("No input stream found; shell is disabled."); while (true) { try { Thread.sleep(86400000); } catch (InterruptedException e) { this._core.getLogger().warn("Shell was interrupted; shell will not run anymore."); } } } /** * Prints an exit message. */ protected static void printExitMessage() { Logger.getLogger("programd").info("Exiting at user request."); } /** * Print a message line of error to the console. * * @param message the message to print */ public void printlnErr(String message) { if (this.midLine) { this.errStream.println(); } this.errStream.println(message); this.midLine = false; } /** * Print a message line of standard output to the console. * * @param message the message to print */ public void printlnOut(String message) { if (this.midLine) { this.outStream.println(); } this.outStream.println(message); this.midLine = false; } /** * Allows an external class to call a command by sending a command line. Prints the command line to the console so * it's possible to see what was attempted. * * @param commandLine the command line to process * @throws NoSuchCommandException if the command line did not contain a command that could be processed */ public void processCommandLine(String commandLine) throws NoSuchCommandException { this.consolePrompt.println(String.format("%s> %s", this.hostname, commandLine)); this.commandRegistry.getHandlerFor(commandLine).handle(commandLine, this); } /** * Displays a line for an interactive console, including the prompt. * * @param preprompt the text to show before the prompt */ protected void promptConsole(String preprompt) { if (this.midLine) { this.consolePrompt.println(); } this.consolePrompt.print(String.format("%s> ", preprompt)); this.midLine = true; } /** * Runs the shell. */ @Override public void run() { if (this._core == null) { throw new NullPointerException("Must attach the shell to a Core before calling run()!"); } this.showMessage(String.format("Interactive shell: type \"/exit\" to shut down; \"%s\" for help.", HelpCommand.COMMAND_STRING)); Bot bot = this.bots.getABot(); if (bot == null) { throw new NullPointerException("No bot to talk to!"); } this.botid = bot.getID(); this.botName = bot.getPropertyValue(this.botNamePredicate); while (true /* && this.core.getStatus() == Core.Status.READY */) { this.showPrompt(); String commandLine = null; try { commandLine = this.inReader.readLine(); } catch (IOException e) { this.noShell(); } this.midLine = false; // Handle commands. if (commandLine != null) { if (commandLine.indexOf('#') == 0) { // Ignore this -- it's a comment. } else if (commandLine.indexOf('/') == 0) { // Exit command if ("/exit".toLowerCase().equals(commandLine)) { Shell.printExitMessage(); this._core.shutdown(); return; } // otherwise... // Try to find a command to handle this. ShellCommand command = null; try { command = this.commandRegistry.getHandlerFor(commandLine); try { command.handle(commandLine, this); } catch (UserError e) { this.showError(String.format("Error processing command: \"%s\"", Errors.describe(e))); } } catch (NoSuchCommandException e) { // May be a commandable. try { this.commandCommandable(commandLine); } catch (NoCommandException ee) { this.showError("Please specify a command following the commandable. For a list of commandables, type \"" + ListCommandablesCommand.COMMAND_STRING + "\"."); } catch (NoSuchCommandableException ee) { this.showError("No such commandable is loaded."); } } } else if (commandLine.length() > 0) { this.showConsole(this.botName, XHTML.breakLines(this._core.getResponse(commandLine, this.hostname, this.botid))); } // If the command line has zero length, ignore it. } } } /** * Displays a multi-line message (after a prompt) in an interactive console. * * @param preprompt the text to show before the prompt * @param message the multi-line message to display */ protected void showConsole(String preprompt, String[] message) { for (String element : message) { this.printlnOut(String.format("%s> %s", preprompt, element)); } } /** * Displays an error message (no prompt) in an interactive console. * * @param message the message to display */ public void showError(String message) { this.printlnErr(message); } /** * Displays a regular message (no prompt) in an interactive console. * * @param message the message to display */ public void showMessage(String message) { this.printlnOut(message); } /** * Displays a prompt. */ protected void showPrompt() { if (this.getState() != Thread.State.NEW) { this.promptConsole('[' + this.botName + "] " + this.predicateMaster.get(this.clientNamePredicate, this.hostname, this.botid).trim()); } } /** * Switches to a bot, given an id. * * @param newBotID */ public void switchToBot(String newBotID) { if (!this.bots.containsKey(newBotID)) { this.showError("That bot id is not known. Check your startup files."); return; } this.botid = newBotID; this.botName = this.bots.get(newBotID).getPropertyValue(this.botNamePredicate); this.showMessage("Switched to bot \"" + newBotID + "\" (name: \"" + this.botName + "\")."); // Send the connect string and print the first response. this.showConsole(this.botName, XHTML.breakLines(this._core.getResponse(this._core.getSettings().getConnectString(), this.hostname, this.botid))); } }