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: * $Id: HenPlus.java,v 1.76 2006/03/19 22:41:57 hzeller Exp $
005: * author: Henner Zeller <H.Zeller@acm.org>
006: */
007: package henplus;
008:
009: import henplus.commands.AboutCommand;
010: import henplus.commands.AliasCommand;
011: import henplus.commands.ConnectCommand;
012: import henplus.commands.DescribeCommand;
013: import henplus.commands.DriverCommand;
014: import henplus.commands.DumpCommand;
015: import henplus.commands.EchoCommand;
016: import henplus.commands.ExitCommand;
017: import henplus.commands.HelpCommand;
018: import henplus.commands.ImportCommand;
019: import henplus.commands.KeyBindCommand;
020: import henplus.commands.ListUserObjectsCommand;
021: import henplus.commands.LoadCommand;
022: import henplus.commands.PluginCommand;
023: import henplus.commands.SQLCommand;
024: import henplus.commands.SetCommand;
025: import henplus.commands.ShellCommand;
026: import henplus.commands.SpoolCommand;
027: import henplus.commands.StatusCommand;
028: import henplus.commands.SystemInfoCommand;
029: import henplus.commands.TreeCommand;
030: import henplus.commands.properties.PropertyCommand;
031: import henplus.commands.properties.SessionPropertyCommand;
032: import henplus.io.ConfigurationContainer;
033:
034: import java.io.BufferedReader;
035: import java.io.EOFException;
036: import java.io.File;
037: import java.util.Iterator;
038: import java.io.IOException;
039: import java.io.InputStream;
040: import org.apache.commons.cli.CommandLine;
041: import org.apache.commons.cli.CommandLineParser;
042: import org.apache.commons.cli.HelpFormatter;
043: import org.apache.commons.cli.Option;
044: import org.apache.commons.cli.Options;
045: import org.apache.commons.cli.ParseException;
046: import org.apache.commons.cli.PosixParser;
047: import java.io.InputStreamReader;
048: import java.io.OutputStream;
049: import java.util.Map;
050:
051: import org.gnu.readline.Readline;
052: import org.gnu.readline.ReadlineLibrary;
053:
054: public class HenPlus implements Interruptable {
055: private static final String HISTORY_NAME = "history";
056: public static final boolean verbose = false; // debug.
057: private static final String HENPLUSDIR = ".henplus";
058: private static final String PROMPT = "Hen*Plus> ";
059:
060: public static final byte LINE_EXECUTED = 1;
061: public static final byte LINE_EMPTY = 2;
062: public static final byte LINE_INCOMPLETE = 3;
063:
064: private static HenPlus instance = null; // singleton.
065:
066: private final boolean _fromTerminal;
067: private final SQLStatementSeparator _commandSeparator;
068: private final StringBuffer _historyLine;
069:
070: private final boolean _quiet;
071: private final ConfigurationContainer _historyConfig;
072:
073: private SetCommand _settingStore;
074: private SessionManager _sessionManager;
075: private CommandDispatcher _dispatcher;
076: private PropertyRegistry _henplusProperties;
077: private ListUserObjectsCommand _objectLister;
078: private String _previousHistoryLine;
079: private boolean _terminated;
080: private String _prompt;
081: private String _emptyPrompt;
082: private File _configDir;
083: private boolean _alreadyShutDown;
084: private BufferedReader _fileReader;
085: private OutputDevice _output;
086: private OutputDevice _msg;
087:
088: private volatile boolean _interrupted;
089:
090: private HenPlus(String argv[]) throws IOException {
091: _terminated = false;
092: _alreadyShutDown = false;
093: boolean quiet = false;
094:
095: _commandSeparator = new SQLStatementSeparator();
096: _historyLine = new StringBuffer();
097: // read options .. like -q
098:
099: try {
100: Readline.load(ReadlineLibrary.GnuReadline);
101: } catch (UnsatisfiedLinkError ignore_me) {
102: System.err
103: .println("no readline found ("
104: + ignore_me.getMessage()
105: + "). Using simple stdin.");
106: }
107:
108: _fromTerminal = Readline.hasTerminal();
109: if (!_fromTerminal && !quiet) {
110: System.err.println("reading from stdin");
111: }
112: _quiet = quiet || !_fromTerminal; // not from terminal: always quiet.
113:
114: if (_fromTerminal) {
115: setOutput(new TerminalOutputDevice(System.out),
116: new TerminalOutputDevice(System.err));
117: } else {
118: setOutput(new PrintStreamOutputDevice(System.out),
119: new PrintStreamOutputDevice(System.err));
120: }
121:
122: if (!_quiet) {
123: System.err
124: .println("using GNU readline (Brian Fox, Chet Ramey), Java wrapper by Bernhard Bablok");
125: }
126: _historyConfig = createConfigurationContainer(HISTORY_NAME);
127: Readline.initReadline("HenPlus");
128: _historyConfig.read(new ConfigurationContainer.ReadAction() {
129: public void readConfiguration(InputStream in)
130: throws Exception {
131: HistoryWriter.readReadlineHistory(in);
132: }
133: });
134:
135: Readline.setWordBreakCharacters(" ,/()<>=\t\n"); // TODO..
136: setDefaultPrompt();
137: }
138:
139: public void initializeCommands(String argv[]) {
140: _henplusProperties = new PropertyRegistry();
141: _henplusProperties.registerProperty("comments-remove",
142: _commandSeparator.getRemoveCommentsProperty());
143:
144: _sessionManager = SessionManager.getInstance();
145:
146: // FIXME: to many cross dependencies of commands now. clean up.
147: _settingStore = new SetCommand(this );
148: _dispatcher = new CommandDispatcher(_settingStore);
149: _objectLister = new ListUserObjectsCommand(this );
150: _henplusProperties.registerProperty("echo-commands",
151: new EchoCommandProperty(_dispatcher));
152:
153: _dispatcher.register(new HelpCommand());
154:
155: /*
156: * this one prints as well the initial copyright header.
157: */
158: _dispatcher.register(new AboutCommand(_quiet));
159:
160: _dispatcher.register(new ExitCommand());
161: _dispatcher.register(new EchoCommand());
162: PluginCommand pluginCommand = new PluginCommand(this );
163: _dispatcher.register(pluginCommand);
164: _dispatcher.register(new DriverCommand(this ));
165: AliasCommand aliasCommand = new AliasCommand(this );
166: _dispatcher.register(aliasCommand);
167: if (_fromTerminal) {
168: _dispatcher.register(new KeyBindCommand(this ));
169: }
170:
171: LoadCommand loadCommand = new LoadCommand();
172: _dispatcher.register(loadCommand);
173:
174: _dispatcher.register(new ConnectCommand(this , _sessionManager));
175: _dispatcher.register(new StatusCommand());
176:
177: _dispatcher.register(_objectLister);
178: _dispatcher.register(new DescribeCommand(_objectLister));
179:
180: _dispatcher.register(new TreeCommand(_objectLister));
181:
182: _dispatcher.register(new SQLCommand(_objectLister,
183: _henplusProperties));
184:
185: _dispatcher.register(new ImportCommand(_objectLister));
186: //_dispatcher.register(new ExportCommand());
187: _dispatcher
188: .register(new DumpCommand(_objectLister, loadCommand));
189:
190: _dispatcher.register(new ShellCommand());
191: _dispatcher.register(new SpoolCommand(this ));
192: _dispatcher.register(_settingStore);
193:
194: PropertyCommand propertyCommand;
195: propertyCommand = new PropertyCommand(this , _henplusProperties);
196: _dispatcher.register(propertyCommand);
197: _dispatcher.register(new SessionPropertyCommand(this ));
198:
199: _dispatcher.register(new SystemInfoCommand());
200:
201: pluginCommand.load();
202: aliasCommand.load();
203: propertyCommand.load();
204:
205: Options opt = new Options();
206: opt.addOption(new Option("h", "help", true,
207: "print this message"));
208: for (Iterator iter = _dispatcher.getRegisteredCommands(); iter
209: .hasNext();) {
210: Command element = (Command) iter.next();
211: try {
212: element.registerOptions(opt);
213: } catch (Throwable e) {
214: System.err.println("while registering " + element);
215: e.printStackTrace();
216: }
217: }
218: CommandLineParser parser = new PosixParser();
219: CommandLine line = null;
220: try {
221: line = parser.parse(opt, argv);
222: for (Iterator iter = _dispatcher.getRegisteredCommands(); iter
223: .hasNext();) {
224: Command element = (Command) iter.next();
225: element.setOptions(opt);
226: element.handleCommandline(line);
227: }
228: } catch (Exception e) {
229: line = null;
230: }
231:
232: if (line == null || opt.hasOption("help")) {
233: HelpFormatter formatter = new HelpFormatter();
234: formatter.printHelp("henplus", opt);
235: System.exit(0);
236: }
237:
238: Readline.setCompleter(_dispatcher);
239:
240: /* FIXME: do this platform independently */
241: // in case someone presses Ctrl-C
242: try {
243: Runtime.getRuntime().addShutdownHook(new Thread() {
244: public void run() {
245: shutdown();
246: }
247: });
248: } catch (NoSuchMethodError e) {
249: // compiled with jdk >= 1.3, executed with <= 1.2.x
250: System.err.println("== This JDK is OLD. ==");
251: System.err.println(" - No final save on CTRL-C supported.");
252: System.err
253: .println(" - and if your shell is broken after use of henplus: same reason.");
254: System.err.println("Bottomline: update your JDK (>= 1.3)!");
255: }
256: /*
257: * if your compiler/system/whatever does not support the sun.misc.*
258: * classes, then just disable this call and the SigIntHandler class.
259: */
260: try {
261: SigIntHandler.install();
262: } catch (Throwable t) {
263: // ignore.
264: }
265:
266: /* TESTING for ^Z support in the shell.
267: sun.misc.SignalHandler stoptest = new sun.misc.SignalHandler () {
268: public void handle(sun.misc.Signal sig) {
269: System.out.println("caught: " + sig);
270: }
271: };
272: try {
273: sun.misc.Signal.handle(new sun.misc.Signal("TSTP"), stoptest);
274: }
275: catch (Exception e) {
276: // ignore.
277: }
278:
279: end testing */
280: }
281:
282: /**
283: * push the current state of the command execution buffer, e.g.
284: * to parse a new file.
285: */
286: public void pushBuffer() {
287: _commandSeparator.push();
288: }
289:
290: /**
291: * pop the command execution buffer.
292: */
293: public void popBuffer() {
294: _commandSeparator.pop();
295: }
296:
297: public String readlineFromFile() throws IOException {
298: if (_fileReader == null) {
299: _fileReader = new BufferedReader(new InputStreamReader(
300: System.in));
301: }
302: String line = _fileReader.readLine();
303: if (line == null) {
304: throw new EOFException("EOF");
305: }
306: return (line.length() == 0) ? null : line;
307: }
308:
309: private void storeLineInHistory() {
310: String line = _historyLine.toString();
311: if (!"".equals(line) && !line.equals(_previousHistoryLine)) {
312: Readline.addToHistory(line);
313: _previousHistoryLine = line;
314: }
315: _historyLine.setLength(0);
316: }
317:
318: /**
319: * add a new line. returns one of LINE_EMPTY, LINE_INCOMPLETE
320: * or LINE_EXECUTED.
321: */
322: public byte executeLine(String line) {
323: byte result = LINE_EMPTY;
324: /*
325: * special oracle comment 'rem'ark; should be in the comment parser.
326: * ONLY if it is on the beginning of the line, no whitespace.
327: */
328: int startWhite = 0;
329: /*
330: while (startWhite < line.length()
331: && Character.isWhitespace(line.charAt(startWhite))) {
332: ++startWhite;
333: }
334: */
335: if (line.length() >= (3 + startWhite)
336: && (line.substring(startWhite, startWhite + 3)
337: .toUpperCase().equals("REM"))
338: && (line.length() == 3 || Character.isWhitespace(line
339: .charAt(3)))) {
340: return LINE_EMPTY;
341: }
342:
343: StringBuffer lineBuf = new StringBuffer(line);
344: lineBuf.append('\n');
345: _commandSeparator.append(lineBuf.toString());
346: result = LINE_INCOMPLETE;
347: while (_commandSeparator.hasNext()) {
348: String completeCommand = _commandSeparator.next();
349: //System.err.println(">'" + completeCommand + "'<");
350: completeCommand = varsubst(completeCommand, _settingStore
351: .getVariableMap());
352: Command c = _dispatcher.getCommandFrom(completeCommand);
353: if (c == null) {
354: _commandSeparator.consumed();
355: /*
356: * do not shadow successful executions with the 'line-empty'
357: * message. Background is: when we consumed a command, that
358: * is complete with a trailing ';', then the following newline
359: * would be considered as empty command. So return only the
360: * LINE_EMPTY, if we haven't got a succesfully executed line.
361: */
362: if (result != LINE_EXECUTED) {
363: result = LINE_EMPTY;
364: }
365: } else if (!c.isComplete(completeCommand)) {
366: _commandSeparator.cont();
367: result = LINE_INCOMPLETE;
368: } else {
369: //System.err.println("SUBST: " + completeCommand);
370: _dispatcher.execute(
371: _sessionManager.getCurrentSession(),
372: completeCommand);
373: _commandSeparator.consumed();
374: result = LINE_EXECUTED;
375: }
376: }
377: return result;
378: }
379:
380: public String getPartialLine() {
381: return _historyLine.toString() + Readline.getLineBuffer();
382: }
383:
384: public void run() {
385: String cmdLine = null;
386: String displayPrompt = _prompt;
387: while (!_terminated) {
388: _interrupted = false;
389: /*
390: * a CTRL-C will not interrupt the current reading
391: * thus it does not make much sense here to interrupt.
392: * WORKAROUND: Print message in the interrupt() method.
393: * TODO: find out, if we can do something that behaves
394: * like a shell. This requires, that CTRL-C makes
395: * Readline.readline() return..
396: */
397: SigIntHandler.getInstance().pushInterruptable(this );
398:
399: try {
400: cmdLine = (_fromTerminal) ? Readline.readline(
401: displayPrompt, false) : readlineFromFile();
402: } catch (EOFException e) {
403: // EOF on CTRL-D
404: if (_sessionManager.getCurrentSession() != null) {
405: _dispatcher.execute(_sessionManager
406: .getCurrentSession(), "disconnect");
407: displayPrompt = _prompt;
408: continue;
409: } else {
410: break; // last session closed -> exit.
411: }
412: } catch (Exception e) {
413: if (verbose)
414: e.printStackTrace();
415: }
416:
417: SigIntHandler.getInstance().reset();
418:
419: // anyone pressed CTRL-C
420: if (_interrupted) {
421: if ((cmdLine == null || cmdLine.trim().length() == 0)
422: && _historyLine.length() == 0) {
423: _terminated = true; // terminate if we press CTRL on empty line.
424: }
425: _historyLine.setLength(0);
426: _commandSeparator.discard();
427: displayPrompt = _prompt;
428: continue;
429: }
430:
431: if (cmdLine == null)
432: continue;
433:
434: /*
435: * if there is already some line in the history, then add
436: * newline. But if the only thing we added was a delimiter (';'),
437: * then this would be annoying.
438: */
439: if (_historyLine.length() > 0
440: && !cmdLine.trim().equals(";")) {
441: _historyLine.append("\n");
442: }
443: _historyLine.append(cmdLine);
444: byte lineExecState = executeLine(cmdLine);
445: if (lineExecState == LINE_INCOMPLETE) {
446: displayPrompt = _emptyPrompt;
447: } else {
448: displayPrompt = _prompt;
449: }
450: if (lineExecState != LINE_INCOMPLETE) {
451: storeLineInHistory();
452: }
453: }
454: SigIntHandler.getInstance().reset();
455: }
456:
457: /**
458: * called at the very end; on signal or called from the shutdown-hook
459: */
460: private void shutdown() {
461: if (_alreadyShutDown) {
462: return;
463: }
464: if (!_quiet) {
465: System.err.println("storing settings..");
466: }
467: /*
468: * allow hard resetting.
469: */
470: SigIntHandler.getInstance().reset();
471: try {
472: if (_dispatcher != null) {
473: _dispatcher.shutdown();
474: }
475: _historyConfig
476: .write(new ConfigurationContainer.WriteAction() {
477: public void writeConfiguration(OutputStream out)
478: throws Exception {
479: HistoryWriter.writeReadlineHistory(out);
480: }
481: });
482: Readline.cleanup();
483: } finally {
484: _alreadyShutDown = true;
485: }
486: /*
487: * some JDBC-Drivers (notably hsqldb) do some important cleanup
488: * (closing open threads, for instance) in finalizers. Force
489: * them to do their duty:
490: */
491: System.gc();
492: System.gc();
493: }
494:
495: public void terminate() {
496: _terminated = true;
497: }
498:
499: public CommandDispatcher getDispatcher() {
500: return _dispatcher;
501: }
502:
503: /**
504: * Provides access to the session manager. He maintains the list of opened
505: * sessions with their names.
506: * @return the session manager.
507: */
508: public SessionManager getSessionManager() {
509: return _sessionManager;
510: }
511:
512: /**
513: * set current session. This is called from commands, that switch
514: * the sessions (i.e. the ConnectCommand.)
515: */
516: public void setCurrentSession(SQLSession session) {
517: getSessionManager().setCurrentSession(session);
518: }
519:
520: /**
521: * get current session.
522: */
523: public SQLSession getCurrentSession() {
524: return getSessionManager().getCurrentSession();
525: }
526:
527: public ListUserObjectsCommand getObjectLister() {
528: return _objectLister;
529: }
530:
531: public void setPrompt(String p) {
532: _prompt = p;
533: StringBuffer tmp = new StringBuffer();
534: int emptyLength = p.length();
535: for (int i = emptyLength; i > 0; --i) {
536: tmp.append(' ');
537: }
538: _emptyPrompt = tmp.toString();
539: // readline won't know anything about these extra characters:
540: // if (_fromTerminal) {
541: // prompt = Terminal.BOLD + prompt + Terminal.NORMAL;
542: // }
543: }
544:
545: public void setDefaultPrompt() {
546: setPrompt(_fromTerminal ? PROMPT : "");
547: }
548:
549: /**
550: * substitute the variables in String 'in', that are in the
551: * form $VARNAME or ${VARNAME} with the equivalent value that is found
552: * in the Map. Return the varsubstituted String.
553: * @param in the input string containing variables to be
554: * substituted (with leading $)
555: * @param variables the Map containing the mapping from variable name
556: * to value.
557: */
558: public String varsubst(String in, Map variables) {
559: int pos = 0;
560: int endpos = 0;
561: int startVar = 0;
562: StringBuffer result = new StringBuffer();
563: String varname;
564: boolean hasBrace = false;
565:
566: if (in == null) {
567: return null;
568: }
569:
570: if (variables == null) {
571: return in;
572: }
573:
574: while ((pos = in.indexOf('$', pos)) >= 0) {
575: startVar = pos;
576: if (in.charAt(pos + 1) == '$') { // quoting '$'
577: result.append(in.substring(endpos, pos));
578: endpos = pos + 1;
579: pos += 2;
580: continue;
581: }
582:
583: hasBrace = (in.charAt(pos + 1) == '{');
584:
585: // text between last variable and here
586: result.append(in.substring(endpos, pos));
587:
588: if (hasBrace) {
589: pos++;
590: }
591:
592: endpos = pos + 1;
593: while (endpos < in.length()
594: && Character
595: .isJavaIdentifierPart(in.charAt(endpos))) {
596: endpos++;
597: }
598: varname = in.substring(pos + 1, endpos);
599:
600: if (hasBrace) {
601: while (endpos < in.length() && in.charAt(endpos) != '}') {
602: ++endpos;
603: }
604: ++endpos;
605: }
606: if (endpos > in.length()) {
607: if (variables.containsKey(varname)) {
608: System.err
609: .println("warning: missing '}' for variable '"
610: + varname + "'.");
611: }
612: result.append(in.substring(startVar));
613: break;
614: }
615:
616: if (variables.containsKey(varname)) {
617: result.append(variables.get(varname));
618: } else {
619: System.err.println("warning: variable '" + varname
620: + "' not set.");
621: result.append(in.substring(startVar, endpos));
622: }
623:
624: pos = endpos;
625: }
626: if (endpos < in.length()) {
627: result.append(in.substring(endpos));
628: }
629: return result.toString();
630: }
631:
632: // -- Interruptable interface
633: public void interrupt() {
634: // watchout: Readline.getLineBuffer() will cause a segmentation fault!
635: getMessageDevice().attributeBold();
636: getMessageDevice().print(
637: " ..discard current line; press [RETURN]");
638: getMessageDevice().attributeReset();
639:
640: _interrupted = true;
641: }
642:
643: //*****************************************************************
644: public static HenPlus getInstance() {
645: return instance;
646: }
647:
648: public void setOutput(OutputDevice out, OutputDevice msg) {
649: _output = out;
650: _msg = msg;
651: }
652:
653: public OutputDevice getOutputDevice() {
654: return _output;
655: }
656:
657: public OutputDevice getMessageDevice() {
658: return _msg;
659: }
660:
661: public static OutputDevice out() {
662: return getInstance().getOutputDevice();
663: }
664:
665: public static OutputDevice msg() {
666: return getInstance().getMessageDevice();
667: }
668:
669: public static final void main(String argv[]) throws Exception {
670: instance = new HenPlus(argv);
671: instance.initializeCommands(argv);
672: instance.run();
673: instance.shutdown();
674: /*
675: * hsqldb does not always stop its log-thread. So do an
676: * explicit exit() here.
677: */
678: System.exit(0);
679: }
680:
681: /**
682: * returns an InputStream for a named configuration. That stream
683: * must be closed on finish.
684: */
685: public ConfigurationContainer createConfigurationContainer(
686: String configName) {
687: return new ConfigurationContainer(new File(getConfigDir(),
688: configName));
689: }
690:
691: public String getConfigurationDirectoryInfo() {
692: return getConfigDir().getAbsolutePath();
693: }
694:
695: private File getConfigDir() {
696: if (_configDir != null) {
697: return _configDir;
698: }
699: /*
700: * test local directory and superdirectories.
701: */
702: File dir = (new File(".")).getAbsoluteFile();
703: while (dir != null) {
704: _configDir = new File(dir, HENPLUSDIR);
705: if (_configDir.exists() && _configDir.isDirectory()) {
706: break;
707: } else {
708: _configDir = null;
709: }
710: dir = dir.getParentFile();
711: }
712:
713: /*
714: * fallback: home directory.
715: */
716: if (_configDir == null) {
717: String homeDir = System.getProperty("user.home", ".");
718: _configDir = new File(homeDir + File.separator + HENPLUSDIR);
719: if (!_configDir.exists()) {
720: if (!_quiet) {
721: System.err.println("creating henplus config dir");
722: }
723: _configDir.mkdir();
724: }
725: try {
726: /*
727: * Make this directory accessible only for the user
728: * in question.
729: * works only on unix. Ignore Exception other OSes
730: */
731: String params[] = new String[] { "chmod", "700",
732: _configDir.toString() };
733: Runtime.getRuntime().exec(params);
734: } catch (Exception e) {
735: if (verbose)
736: e.printStackTrace();
737: }
738: }
739: _configDir = _configDir.getAbsoluteFile();
740: try {
741: _configDir = _configDir.getCanonicalFile();
742: } catch (IOException ign) { /* ign */
743: }
744:
745: if (!_quiet) {
746: System.err.println("henplus config at " + _configDir);
747: }
748: return _configDir;
749: }
750: }
751:
752: /*
753: * Local variables:
754: * c-basic-offset: 4
755: * compile-command: "ant -emacs -find build.xml"
756: * End:
757: */
|