/*
* This is free software, licensed under the Gnu Public License (GPL) get a copy from <http://www.gnu.org/licenses/gpl.html> $Id:
* HenPlus.java,v 1.77 2008-10-19 09:14:49 hzeller Exp $ author: Henner Zeller <H.Zeller@acm.org>
*/
package henplus;
import henplus.commands.AboutCommand;
import henplus.commands.AliasCommand;
import henplus.commands.ConnectCommand;
import henplus.commands.DescribeCommand;
import henplus.commands.DriverCommand;
import henplus.commands.DumpCommand;
import henplus.commands.EchoCommand;
import henplus.commands.ExitCommand;
import henplus.commands.HelpCommand;
import henplus.commands.ImportCommand;
import henplus.commands.KeyBindCommand;
import henplus.commands.ListUserObjectsCommand;
import henplus.commands.LoadCommand;
import henplus.commands.PluginCommand;
import henplus.commands.SQLCommand;
import henplus.commands.SetCommand;
import henplus.commands.ShellCommand;
import henplus.commands.SpoolCommand;
import henplus.commands.StatusCommand;
import henplus.commands.SystemInfoCommand;
import henplus.commands.TreeCommand;
import henplus.commands.properties.PropertyCommand;
import henplus.commands.properties.SessionPropertyCommand;
import henplus.io.ConfigurationContainer;
import henplus.logging.Logger;
import henplus.util.StringUtil;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.gnu.readline.Readline;
import org.gnu.readline.ReadlineLibrary;
public final class HenPlus implements Interruptable {
private static final String HISTORY_NAME = "history";
private static final String HENPLUSDIR = ".henplus";
private static final String PROMPT = "Hen*Plus> ";
public static final byte LINE_EXECUTED = 1;
public static final byte LINE_EMPTY = 2;
public static final byte LINE_INCOMPLETE = 3;
private static HenPlus instance = null; // singleton.
private boolean _fromTerminal;
private final SQLStatementSeparator _commandSeparator;
private final StringBuilder _historyLine;
private boolean _quiet;
private ConfigurationContainer _historyConfig;
private SetCommand _settingStore;
private SessionManager _sessionManager;
private CommandDispatcher _dispatcher;
private PropertyRegistry _henplusProperties;
private ListUserObjectsCommand _objectLister;
private String _previousHistoryLine;
private boolean _terminated;
private String _prompt;
private String _emptyPrompt;
private File _configDir;
private boolean _alreadyShutDown;
private BufferedReader _fileReader;
private OutputDevice _output;
private OutputDevice _msg;
private volatile boolean _interrupted;
private boolean _verbose;
/**
* Only initialize fields so that the instance is up fast.
*/
private HenPlus() throws IOException {
_terminated = false;
_alreadyShutDown = false;
_commandSeparator = new SQLStatementSeparator();
_historyLine = new StringBuilder();
// read options .. like -q
}
/**
* @param argv
* @throws UnsupportedEncodingException
*/
private void init(final String[] argv) throws UnsupportedEncodingException {
String noReadlineMsg = null;
try {
Readline.load(ReadlineLibrary.GnuReadline);
} catch (final UnsatisfiedLinkError ignoreMe) {
noReadlineMsg = String.format("no readline found (%s). Using simple stdin.", ignoreMe.getMessage());
}
_fromTerminal = Readline.hasTerminal();
_quiet |= !_fromTerminal; // not from terminal: always quiet.
if (_fromTerminal) {
setOutput(new TerminalOutputDevice(System.out), new TerminalOutputDevice(System.err));
} else {
setOutput(new PrintStreamOutputDevice(System.out), new PrintStreamOutputDevice(System.err));
}
initializeCommands(argv);
readCommandLineOptions(argv);
if (StringUtil.isEmpty(noReadlineMsg)) {
Logger.info("using GNU readline (Brian Fox, Chet Ramey), Java wrapper by Bernhard Bablok");
} else {
Logger.info(noReadlineMsg);
}
_historyConfig = createConfigurationContainer(HISTORY_NAME);
Readline.initReadline("HenPlus");
_historyConfig.read(new ConfigurationContainer.ReadAction() {
@Override
public void readConfiguration(final InputStream in) throws Exception {
HistoryWriter.readReadlineHistory(in);
}
});
Readline.setWordBreakCharacters(" ,/()<>=\t\n"); // TODO..
setDefaultPrompt();
}
public void initializeCommands(final String[] argv) {
_henplusProperties = new PropertyRegistry();
_henplusProperties.registerProperty("comments-remove", _commandSeparator.getRemoveCommentsProperty());
_sessionManager = SessionManager.getInstance();
// FIXME: to many cross dependencies of commands now. clean up.
_settingStore = new SetCommand(this);
_dispatcher = new CommandDispatcher(_settingStore);
_objectLister = new ListUserObjectsCommand(this);
_henplusProperties.registerProperty("echo-commands", new EchoCommandProperty(_dispatcher));
_dispatcher.register(new HelpCommand());
/*
* this one prints as well the initial copyright header.
*/
_dispatcher.register(new AboutCommand());
_dispatcher.register(new ExitCommand());
_dispatcher.register(new EchoCommand());
final PluginCommand pluginCommand = new PluginCommand(this);
_dispatcher.register(pluginCommand);
_dispatcher.register(new DriverCommand(this));
final AliasCommand aliasCommand = new AliasCommand(this);
_dispatcher.register(aliasCommand);
if (_fromTerminal) {
_dispatcher.register(new KeyBindCommand(this));
}
final LoadCommand loadCommand = new LoadCommand();
_dispatcher.register(loadCommand);
_dispatcher.register(new ConnectCommand(this, _sessionManager));
_dispatcher.register(new StatusCommand());
_dispatcher.register(_objectLister);
_dispatcher.register(new DescribeCommand(_objectLister));
_dispatcher.register(new TreeCommand(_objectLister));
_dispatcher.register(new SQLCommand(_objectLister, _henplusProperties));
_dispatcher.register(new ImportCommand(_objectLister));
// _dispatcher.register(new ExportCommand());
_dispatcher.register(new DumpCommand(_objectLister, loadCommand));
_dispatcher.register(new ShellCommand());
_dispatcher.register(new SpoolCommand(this));
_dispatcher.register(_settingStore);
PropertyCommand propertyCommand;
propertyCommand = new PropertyCommand(this, _henplusProperties);
_dispatcher.register(propertyCommand);
_dispatcher.register(new SessionPropertyCommand(this));
_dispatcher.register(new SystemInfoCommand());
pluginCommand.load();
aliasCommand.load();
propertyCommand.load();
Readline.setCompleter(_dispatcher);
/* FIXME: do this platform independently */
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
shutdown();
}
});
/*
* if your compiler/system/whatever does not support the sun.misc.
* classes, then just disable this call and the SigIntHandler class.
*/
try {
SigIntHandler.install();
} catch (final Throwable t) {
// ignore.
}
/*
* TESTING for ^Z support in the shell. sun.misc.SignalHandler stoptest
* = new sun.misc.SignalHandler () { public void handle(sun.misc.Signal
* sig) { System.out.println("caught: " + sig); } }; try {
* sun.misc.Signal.handle(new sun.misc.Signal("TSTP"), stoptest); }
* catch (Exception e) { // ignore. }
*
* end testing
*/
}
/**
* @param argv
*/
private void readCommandLineOptions(final String[] argv) {
final Options availableOptions = getMainOptions();
registerCommandOptions(availableOptions);
final CommandLineParser parser = new PosixParser();
CommandLine line = null;
try {
line = parser.parse(availableOptions, argv);
if (line.hasOption('h')) {
usageAndExit(availableOptions, 0);
}
if (line.hasOption('s')) {
_quiet = true;
}
if (line.hasOption('v')) {
_verbose = true;
}
handleCommandOptions(line);
} catch (final Exception e) {
Logger.error("Error handling command line arguments", e);
usageAndExit(availableOptions, 1);
}
}
/**
* @param availableOptions
*/
private void usageAndExit(final Options availableOptions, final int returnCode) {
final HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("henplus", availableOptions);
System.exit(returnCode);
}
/**
* @param line
*/
private void handleCommandOptions(final CommandLine line) {
for (final Iterator<Command> it = _dispatcher.getRegisteredCommands(); it.hasNext();) {
final Command element = it.next();
element.handleCommandline(line);
}
}
/**
* @param availableOptions
*/
private void registerCommandOptions(final Options availableOptions) {
for (final Iterator<Command> it = _dispatcher.getRegisteredCommands(); it.hasNext();) {
final Command element = it.next();
try {
for (final Option option : element.getHandledCommandLineOptions()) {
availableOptions.addOption(option);
}
} catch (final Throwable e) {
Logger.error("while registering %s", e, element);
e.printStackTrace();
}
}
}
/**
* @return
*/
private Options getMainOptions() {
final Options availableOptions = new Options();
availableOptions.addOption(new Option("h", "help", false, "print this message"));
availableOptions.addOption(new Option("s", "silent", false, "suppress all output except query results"));
availableOptions.addOption(new Option("v", "verbose", false, "print debug output"));
return availableOptions;
}
/**
* push the current state of the command execution buffer, e.g. to parse a new file.
*/
public void pushBuffer() {
_commandSeparator.push();
}
/**
* pop the command execution buffer.
*/
public void popBuffer() {
_commandSeparator.pop();
}
public String readlineFromFile() throws IOException {
if (_fileReader == null) {
_fileReader = new BufferedReader(new InputStreamReader(System.in));
}
final String line = _fileReader.readLine();
if (line == null) {
throw new EOFException("EOF");
}
return line.length() == 0 ? null : line;
}
private void storeLineInHistory() {
final String line = _historyLine.toString();
if (!"".equals(line) && !line.equals(_previousHistoryLine)) {
Readline.addToHistory(line);
_previousHistoryLine = line;
}
_historyLine.setLength(0);
}
/**
* add a new line. returns one of LINE_EMPTY, LINE_INCOMPLETE or LINE_EXECUTED.
*/
public byte executeLine(final String line) {
byte result = LINE_EMPTY;
/*
* special oracle comment 'rem'ark; should be in the comment parser.
* ONLY if it is on the beginning of the line, no whitespace.
*/
final int startWhite = 0;
/*
* while (startWhite < line.length() &&
* Character.isWhitespace(line.charAt(startWhite))) { ++startWhite; }
*/
if (line.length() >= 3 + startWhite && line.substring(startWhite, startWhite + 3).toUpperCase().equals("REM")
&& (line.length() == 3 || Character.isWhitespace(line.charAt(3)))) {
return LINE_EMPTY;
}
final StringBuilder lineBuf = new StringBuilder(line);
lineBuf.append('\n');
_commandSeparator.append(lineBuf.toString());
result = LINE_INCOMPLETE;
while (_commandSeparator.hasNext()) {
String completeCommand = _commandSeparator.next();
completeCommand = varsubst(completeCommand, _settingStore.getVariableMap());
final Command c = _dispatcher.getCommandFrom(completeCommand);
if (c == null) {
_commandSeparator.consumed();
/*
* do not shadow successful executions with the 'line-empty'
* message. Background is: when we consumed a command, that is
* complete with a trailing ';', then the following newline
* would be considered as empty command. So return only the
* LINE_EMPTY, if we haven't got a succesfully executed line.
*/
if (result != LINE_EXECUTED) {
result = LINE_EMPTY;
}
} else if (!c.isComplete(completeCommand)) {
_commandSeparator.cont();
result = LINE_INCOMPLETE;
} else {
_dispatcher.execute(_sessionManager.getCurrentSession(), completeCommand);
_commandSeparator.consumed();
result = LINE_EXECUTED;
}
}
return result;
}
public String getPartialLine() {
return _historyLine.toString() + Readline.getLineBuffer();
}
public void run() {
String cmdLine = null;
String displayPrompt = _prompt;
while (!_terminated) {
_interrupted = false;
/*
* a CTRL-C will not interrupt the current reading thus it does not
* make much sense here to interrupt. WORKAROUND: Print message in
* the interrupt() method. TODO: find out, if we can do something
* that behaves like a shell. This requires, that CTRL-C makes
* Readline.readline() return..
*/
SigIntHandler.getInstance().pushInterruptable(this);
try {
cmdLine = _fromTerminal ? Readline.readline(displayPrompt, false) : readlineFromFile();
} catch (final EOFException e) {
// EOF on CTRL-D
if (_sessionManager.getCurrentSession() != null) {
_dispatcher.execute(_sessionManager.getCurrentSession(), "disconnect");
displayPrompt = _prompt;
continue;
} else {
break; // last session closed -> exit.
}
} catch (final Exception e) {
if (_verbose) {
e.printStackTrace();
}
}
SigIntHandler.getInstance().reset();
// anyone pressed CTRL-C
if (_interrupted) {
_historyLine.setLength(0);
_commandSeparator.discard();
displayPrompt = _prompt;
continue;
}
if (cmdLine == null) {
continue;
}
/*
* if there is already some line in the history, then add newline.
* But if the only thing we added was a delimiter (';'), then this
* would be annoying.
*/
if (_historyLine.length() > 0 && !cmdLine.trim().equals(";")) {
_historyLine.append("\n");
}
_historyLine.append(cmdLine);
final byte lineExecState = executeLine(cmdLine);
if (lineExecState == LINE_INCOMPLETE) {
displayPrompt = _emptyPrompt;
} else {
displayPrompt = _prompt;
}
if (lineExecState != LINE_INCOMPLETE) {
storeLineInHistory();
}
}
SigIntHandler.getInstance().reset();
}
/**
* called at the very end; on signal or called from the shutdown-hook
*/
private void shutdown() {
if (_alreadyShutDown) {
return;
}
Logger.info("storing settings..");
/*
* allow hard resetting.
*/
SigIntHandler.getInstance().reset();
try {
if (_dispatcher != null) {
_dispatcher.shutdown();
}
if (_historyConfig != null) {
_historyConfig.write(new ConfigurationContainer.WriteAction() {
@Override
public void writeConfiguration(final OutputStream out) throws Exception {
HistoryWriter.writeReadlineHistory(out);
}
});
}
Readline.cleanup();
} finally {
_alreadyShutDown = true;
}
/*
* some JDBC-Drivers (notably hsqldb) do some important cleanup (closing
* open threads, for instance) in finalizers. Force them to do their
* duty:
*/
System.gc();
System.gc();
}
public void terminate() {
_terminated = true;
}
public CommandDispatcher getDispatcher() {
return _dispatcher;
}
/**
* Provides access to the session manager. He maintains the list of opened sessions with their names.
*
* @return the session manager.
*/
public SessionManager getSessionManager() {
return _sessionManager;
}
/**
* set current session. This is called from commands, that switch the sessions (i.e. the ConnectCommand.)
*/
public void setCurrentSession(final SQLSession session) {
getSessionManager().setCurrentSession(session);
}
/**
* get current session.
*/
public SQLSession getCurrentSession() {
return getSessionManager().getCurrentSession();
}
public ListUserObjectsCommand getObjectLister() {
return _objectLister;
}
public void setPrompt(final String p) {
_prompt = p;
final StringBuilder tmp = new StringBuilder();
final int emptyLength = p.length();
for (int i = emptyLength; i > 0; --i) {
tmp.append(' ');
}
_emptyPrompt = tmp.toString();
// readline won't know anything about these extra characters:
// if (_fromTerminal) {
// prompt = Terminal.BOLD + prompt + Terminal.NORMAL;
// }
}
public void setDefaultPrompt() {
setPrompt(_fromTerminal ? PROMPT : "");
}
/**
* substitute the variables in String 'in', that are in the form $VARNAME or ${VARNAME} with the equivalent value that is found
* in the Map. Return the varsubstituted String.
*
* @param in
* the input string containing variables to be substituted (with leading $)
* @param variables
* the Map containing the mapping from variable name to value.
*/
public String varsubst(final String in, final Map<String,String> variables) {
int pos = 0;
int endpos = 0;
int startVar = 0;
final StringBuilder result = new StringBuilder();
String varname;
boolean hasBrace = false;
if (in == null) {
return null;
}
if (variables == null) {
return in;
}
while ((pos = in.indexOf('$', pos)) >= 0) {
startVar = pos;
if (in.charAt(pos + 1) == '$') { // quoting '$'
result.append(in.substring(endpos, pos));
endpos = pos + 1;
pos += 2;
continue;
}
hasBrace = in.charAt(pos + 1) == '{';
// text between last variable and here
result.append(in.substring(endpos, pos));
if (hasBrace) {
pos++;
}
endpos = pos + 1;
while (endpos < in.length() && Character.isJavaIdentifierPart(in.charAt(endpos))) {
endpos++;
}
varname = in.substring(pos + 1, endpos);
if (hasBrace) {
while (endpos < in.length() && in.charAt(endpos) != '}') {
++endpos;
}
++endpos;
}
if (endpos > in.length()) {
if (variables.containsKey(varname)) {
Logger.info("warning: missing '}' for variable '%s'.", varname);
}
result.append(in.substring(startVar));
break;
}
if (variables.containsKey(varname)) {
result.append(variables.get(varname));
} else {
Logger.info("warning: variable '%s' not set.", varname);
result.append(in.substring(startVar, endpos));
}
pos = endpos;
}
if (endpos < in.length()) {
result.append(in.substring(endpos));
}
return result.toString();
}
// -- Interruptable interface
@Override
public void interrupt() {
// watchout: Readline.getLineBuffer() will cause a segmentation fault!
getMessageDevice().attributeBold();
getMessageDevice().print("\n...discarded current command line; press [RETURN] to continue or [CTRL-D] to exit henplus");
getMessageDevice().attributeReset();
_interrupted = true;
}
// *****************************************************************
public static HenPlus getInstance() {
return instance;
}
public void setOutput(final OutputDevice out, final OutputDevice msg) {
_output = out;
_msg = msg;
}
public OutputDevice getOutputDevice() {
return _output;
}
public OutputDevice getMessageDevice() {
return _msg;
}
public static OutputDevice out() {
return getInstance().getOutputDevice();
}
public static OutputDevice msg() {
return getInstance().getMessageDevice();
}
public static void main(final String[] argv) throws Exception {
instance = new HenPlus();
instance.init(argv);
instance.run();
instance.shutdown();
/*
* hsqldb does not always stop its log-thread. So do an explicit exit()
* here.
*/
System.exit(0);
}
/**
* returns an InputStream for a named configuration. That stream must be closed on finish.
*/
public ConfigurationContainer createConfigurationContainer(final String configName) {
return new ConfigurationContainer(new File(getConfigDir(), configName));
}
public String getConfigurationDirectoryInfo() {
return getConfigDir().getAbsolutePath();
}
private File getConfigDir() {
if (_configDir != null) {
return _configDir;
}
/*
* test local directory and superdirectories.
*/
File dir = new File(".").getAbsoluteFile();
while (dir != null) {
_configDir = new File(dir, HENPLUSDIR);
if (_configDir.exists() && _configDir.isDirectory()) {
break;
} else {
_configDir = null;
}
dir = dir.getParentFile();
}
/*
* fallback: home directory.
*/
if (_configDir == null) {
final String homeDir = System.getProperty("user.home", ".");
_configDir = new File(homeDir + File.separator + HENPLUSDIR);
if (!_configDir.exists()) {
Logger.debug("creating henplus config dir.");
if (!_configDir.mkdir()) {
Logger.error("henplus config dir at '%s' could not be created.", _configDir.getAbsolutePath());
}
}
try {
/*
* Make this directory accessible only for the user in question.
* works only on unix. Ignore Exception other OSes
*/
final String[] params = new String[] { "chmod", "700", _configDir.toString() };
Runtime.getRuntime().exec(params);
} catch (final Exception e) {
if (_verbose) {
e.printStackTrace();
}
}
}
_configDir = _configDir.getAbsoluteFile();
try {
_configDir = _configDir.getCanonicalFile();
} catch (final IOException ign) { /* ign */
}
Logger.info("henplus config at " + _configDir, false);
return _configDir;
}
public boolean isQuiet() {
return _quiet;
}
public boolean isVerbose() {
return _verbose;
}
}
/*
* Local variables: c-basic-offset: 4 compile-command:
* "ant -emacs -find build.xml" End:
*/