001: /*
002: * This is free software, licensed under the Gnu Public License (GPL)
003: * get a copy from <http://www.gnu.org/licenses/gpl.html>
004: *
005: * author: Henner Zeller <H.Zeller@acm.org>
006: */
007: package henplus;
008:
009: import henplus.event.ExecutionListener;
010: import henplus.commands.SetCommand;
011:
012: import java.util.ArrayList;
013: import java.util.Enumeration;
014: import java.util.Iterator;
015: import java.util.List;
016: import java.util.Map;
017: import java.util.SortedMap;
018: import java.util.StringTokenizer;
019: import java.util.TreeMap;
020:
021: import org.gnu.readline.ReadlineCompleter;
022:
023: /**
024: * The Command Dispatcher for all commands.
025: */
026: public class CommandDispatcher implements ReadlineCompleter {
027: private final static boolean verbose = false; // debug
028: private final List/*<Command>*/commands; // commands in seq. of addition.
029: private final SortedMap commandMap;
030: private final SetCommand setCommand;
031: private final List/*<ExecutionL<Listener>*/executionListeners;
032: private int _batchCount;
033:
034: public CommandDispatcher(SetCommand sc) {
035: commandMap = new TreeMap();
036: commands = new ArrayList();
037: executionListeners = new ArrayList();
038: _batchCount = 0;
039: setCommand = sc;
040: // FIXME: remove cyclic dependency..
041: setCommand.registerLastCommandListener(this );
042: }
043:
044: /**
045: * returns the commands in the sequence they have been added.
046: */
047: public Iterator getRegisteredCommands() {
048: return commands.iterator();
049: }
050:
051: /**
052: * returns a sorted list of command names.
053: */
054: public Iterator getRegisteredCommandNames() {
055: return commandMap.keySet().iterator();
056: }
057:
058: /**
059: * returns a sorted list of command names, starting with the first entry
060: * matching the key.
061: */
062: public Iterator getRegisteredCommandNames(String key) {
063: return commandMap.tailMap(key).keySet().iterator();
064: }
065:
066: /*
067: * if we start a batch (reading from file), the commands are not shown,
068: * except the commands that failed.
069: */
070: public void startBatch() {
071: ++_batchCount;
072: }
073:
074: public void endBatch() {
075: --_batchCount;
076: }
077:
078: public boolean isInBatch() {
079: return _batchCount > 0;
080: }
081:
082: public void register(Command c) {
083: commands.add(c);
084: String[] cmdStrings = c.getCommandList();
085: for (int i = 0; i < cmdStrings.length; ++i) {
086: if (commandMap.containsKey(cmdStrings[i]))
087: throw new IllegalArgumentException(
088: "attempt to register command '" + cmdStrings[i]
089: + "', that is already used");
090: commandMap.put(cmdStrings[i], c);
091: }
092: }
093:
094: // methods to make aliases work.
095: public boolean containsCommand(String cmd) {
096: return commandMap.containsKey(cmd);
097: }
098:
099: public void registerAdditionalCommand(String cmd, Command c) {
100: commandMap.put(cmd, c);
101: }
102:
103: public void unregisterAdditionalCommand(String cmd) {
104: commandMap.remove(cmd);
105: }
106:
107: /**
108: * unregister command. This is an 'expensive' operation, since we
109: * go through the internal list until we find the command and remove
110: * it. But since the number of commands is low and this is a rare
111: * operation (the plugin-mechanism does this) .. we don't care.
112: */
113: public void unregister(Command c) {
114: commands.remove(c);
115: Iterator entries = commandMap.entrySet().iterator();
116: while (entries.hasNext()) {
117: Map.Entry e = (Map.Entry) entries.next();
118: if (e.getValue() == c) {
119: entries.remove();
120: }
121: }
122: }
123:
124: /**
125: * extracts the command from the commandstring. This even works, if there
126: * is not delimiter between the command and its arguments (this is esp.
127: * needed for the commands '?', '!', '@' and '@@').
128: */
129: public String getCommandNameFrom(String completeCmd) {
130: if (completeCmd == null || completeCmd.length() == 0)
131: return null;
132: String cmd = completeCmd.toLowerCase();
133: final String startChar = cmd.substring(0, 1);
134: Iterator it = getRegisteredCommandNames(startChar);
135: String longestMatch = null;
136: while (it.hasNext()) {
137: String testMatch = (String) it.next();
138: if (cmd.startsWith(testMatch)) {
139: longestMatch = testMatch;
140: } else if (!testMatch.startsWith(startChar)) {
141: break; // ok, thats it.
142: }
143: }
144: // ok, fallback: grab the first whitespace delimited part.
145: if (longestMatch == null) {
146: Enumeration tok = new StringTokenizer(completeCmd,
147: " ;\t\n\r\f");
148: if (tok.hasMoreElements()) {
149: return (String) tok.nextElement();
150: }
151: }
152: return longestMatch;
153: }
154:
155: public Command getCommandFrom(String completeCmd) {
156: return getCommandFromCooked(getCommandNameFrom(completeCmd));
157: }
158:
159: private Command getCommandFromCooked(String completeCmd) {
160: if (completeCmd == null)
161: return null;
162: Command c = (Command) commandMap.get(completeCmd);
163: if (c == null) {
164: c = (Command) commandMap.get(""); // "" matches everything.
165: }
166: return c;
167: }
168:
169: public void shutdown() {
170: Iterator i = commands.iterator();
171: while (i.hasNext()) {
172: Command c = (Command) i.next();
173: try {
174: c.shutdown();
175: } catch (Exception e) {
176: if (verbose)
177: e.printStackTrace();
178: }
179: }
180: }
181:
182: /**
183: * Add an execution listener that is informed whenever a command
184: * is executed.
185: * @param listener an Execution Listener
186: */
187: public void addExecutionListener(ExecutionListener listener) {
188: if (!executionListeners.contains(listener)) {
189: executionListeners.add(listener);
190: }
191: }
192:
193: /**
194: * remove an execution listener.
195: * @param listener the execution listener to be removed
196: * @return true, if this has been successful.
197: */
198: public boolean removeExecutionListener(ExecutionListener listener) {
199: return executionListeners.remove(listener);
200: }
201:
202: private void informBeforeListeners(SQLSession session, String cmd) {
203: Iterator it = executionListeners.iterator();
204: while (it.hasNext()) {
205: ExecutionListener listener = (ExecutionListener) it.next();
206: listener.beforeExecution(session, cmd);
207: }
208: }
209:
210: private void informAfterListeners(SQLSession session, String cmd,
211: int result) {
212: Iterator it = executionListeners.iterator();
213: while (it.hasNext()) {
214: ExecutionListener listener = (ExecutionListener) it.next();
215: listener.afterExecution(session, cmd, result);
216: }
217: }
218:
219: /**
220: * execute the command given. This strips whitespaces and trailing
221: * semicolons and calls the Command class.
222: */
223: public void execute(SQLSession session, final String givenCommand) {
224: if (givenCommand == null)
225: return;
226:
227: // remove trailing ';' and whitespaces.
228: StringBuffer cmdBuf = new StringBuffer(givenCommand.trim());
229: int i = 0;
230: for (i = cmdBuf.length() - 1; i > 0; --i) {
231: char c = cmdBuf.charAt(i);
232: if (c != ';' && !Character.isWhitespace(c))
233: break;
234: }
235: if (i < 0) {
236: return;
237: }
238: cmdBuf.setLength(i + 1);
239: String cmd = cmdBuf.toString();
240: //System.err.println("## '" + cmd + "'");
241: String cmdStr = getCommandNameFrom(cmd);
242: Command c = getCommandFromCooked(cmdStr);
243: //System.err.println("name: "+ cmdStr + "; c=" + c);
244: if (c != null) {
245: try {
246: String params = cmd.substring(cmdStr.length());
247: if (session == null && c.requiresValidSession(cmdStr)) {
248: HenPlus.msg().println("not connected.");
249: return;
250: }
251:
252: int result;
253: informBeforeListeners(session, givenCommand);
254: result = c.execute(session, cmdStr, params);
255: informAfterListeners(session, givenCommand, result);
256:
257: switch (result) {
258: case Command.SYNTAX_ERROR: {
259: String synopsis = c.getSynopsis(cmdStr);
260: if (synopsis != null) {
261: HenPlus.msg().println("usage: " + synopsis);
262: } else {
263: HenPlus.msg().println("syntax error.");
264: }
265: }
266: break;
267: case Command.EXEC_FAILED: {
268: /*
269: * if we are in batch mode, then no message is written
270: * to the screen by default. Thus we don't know, _what_
271: * command actually failed. So in this case, write out
272: * the offending command.
273: */
274: if (isInBatch()) {
275: HenPlus.msg().println("-- failed command: ");
276: HenPlus.msg().println(givenCommand);
277: }
278: }
279: break;
280: default:
281: /* nope */
282: }
283: } catch (Throwable e) {
284: if (verbose)
285: e.printStackTrace();
286: HenPlus.msg().println(e.toString());
287: informAfterListeners(session, givenCommand,
288: Command.EXEC_FAILED);
289: }
290: }
291: }
292:
293: private Iterator possibleValues;
294: private String variablePrefix;
295:
296: //-- Readline completer ..
297: public String completer(String text, int state) {
298: final HenPlus henplus = HenPlus.getInstance();
299: String completeCommandString = henplus.getPartialLine().trim();
300: boolean variableExpansion = false;
301:
302: /*
303: * ok, do we have a variable expansion ?
304: */
305: int pos = text.length() - 1;
306: while (pos > 0 && (text.charAt(pos) != '$')
307: && Character.isJavaIdentifierPart(text.charAt(pos))) {
308: --pos;
309: }
310: // either $... or ${...
311: if ((pos >= 0 && text.charAt(pos) == '$')) {
312: variableExpansion = true;
313: } else if ((pos >= 1) && text.charAt(pos - 1) == '$'
314: && text.charAt(pos) == '{') {
315: variableExpansion = true;
316: --pos;
317: }
318:
319: if (variableExpansion) {
320: if (state == 0) {
321: variablePrefix = text.substring(0, pos);
322: String varname = text.substring(pos);
323: possibleValues = setCommand.completeUserVar(varname);
324: }
325: if (possibleValues.hasNext()) {
326: return variablePrefix
327: + ((String) possibleValues.next());
328: }
329: return null;
330: }
331: /*
332: * the first word.. the command.
333: */
334: else if (completeCommandString.equals(text)) {
335: text = text.toLowerCase();
336: if (state == 0) {
337: possibleValues = getRegisteredCommandNames(text);
338: }
339: while (possibleValues.hasNext()) {
340: String nextKey = (String) possibleValues.next();
341: if (nextKey.length() == 0)// don't complete the 'empty' thing.
342: continue;
343: if (text.length() < 1) {
344: Command c = (Command) commandMap.get(nextKey);
345: if (!c.participateInCommandCompletion())
346: continue;
347: if (c.requiresValidSession(nextKey)
348: && henplus.getCurrentSession() == null) {
349: continue;
350: }
351: }
352: if (nextKey.startsWith(text))
353: return nextKey;
354: return null;
355: }
356: return null;
357: }
358: /*
359: * .. otherwise get completion from the specific command.
360: */
361: else {
362: if (state == 0) {
363: Command cmd = getCommandFrom(completeCommandString);
364: if (cmd == null) {
365: return null;
366: }
367: possibleValues = cmd.complete(this ,
368: completeCommandString, text);
369: }
370: if (possibleValues != null && possibleValues.hasNext()) {
371: return (String) possibleValues.next();
372: }
373: return null;
374: }
375: }
376: }
377:
378: /*
379: * Local variables:
380: * c-basic-offset: 4
381: * compile-command: "ant -emacs -find build.xml"
382: * End:
383: */
|