package edu.brown.terminal;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jline.ConsoleReader;
import org.apache.log4j.Logger;
import org.voltdb.VoltTable;
import org.voltdb.VoltType;
import org.voltdb.catalog.Catalog;
import org.voltdb.catalog.Database;
import org.voltdb.catalog.ProcParameter;
import org.voltdb.catalog.Procedure;
import org.voltdb.catalog.Site;
import org.voltdb.client.Client;
import org.voltdb.client.ClientFactory;
import org.voltdb.client.ClientResponse;
import org.voltdb.client.NoConnectionsException;
import org.voltdb.types.TimestampType;
import org.voltdb.utils.NotImplementedException;
import org.voltdb.utils.VoltTableUtil;
import org.voltdb.utils.VoltTypeUtil;
import edu.brown.catalog.CatalogUtil;
import edu.brown.hstore.HStoreConstants;
import edu.brown.hstore.Hstoreservice.Status;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
import edu.brown.utils.ArgumentsParser;
import edu.brown.utils.CollectionUtil;
import edu.brown.utils.StringUtil;
/**
* H-Store Commandline Client Terminal
* @author gen
* @author pavlo
*/
public class HStoreTerminal implements Runnable {
private static final Logger LOG = Logger.getLogger(HStoreTerminal.class);
private static final LoggerBoolean debug = new LoggerBoolean();
/**
* Special non-standard commands that we can execute
* These are to help us mimic MySQL
*/
public enum Command {
DESCRIBE("Not Implemented"),
EXEC("ProcedureName [OptionalParams]"),
ENABLE("OptionName"),
SHOW("Not Implemented"),
QUIT("");
private final String usage;
private Command(String usage) {
this.usage = usage;
}
};
private class TerminalConnection {
final Client client;
final String hostname;
final int port;
public TerminalConnection(Client client, String hostname, int port) {
this.client = client;
this.hostname = hostname;
this.port = port;
}
} // CLASS
// ---------------------------------------------------------------
// STATIC CONFIGURATION MEMBERS
// ---------------------------------------------------------------
private static final String setPlainText = StringUtil.SET_PLAIN_TEXT;
private static final String setBoldGreenText = "\033[1;32m"; // 0;1m";
private static final String setBoldText = "\033[0;1m";
private static final String PROMPT = setBoldGreenText + "hstore>" + setPlainText + " ";
private static final Pattern SPLITTER = Pattern.compile("[ ]+");
// ---------------------------------------------------------------
// INSTANCE CONFIGURATION MEMBERS
// ---------------------------------------------------------------
private final Catalog catalog;
private final Database catalog_db;
private final jline.ConsoleReader reader = new ConsoleReader();
private final TokenCompletor completer;
// OPTIONS
private boolean enable_csv = false;
private boolean enable_debug = false;
private String hostname = null;
private int port = HStoreConstants.DEFAULT_PORT;
// ---------------------------------------------------------------
// CONSTRUCTOR
// ---------------------------------------------------------------
public HStoreTerminal(Catalog catalog) throws Exception{
this.catalog = catalog;
this.catalog_db = CatalogUtil.getDatabase(this.catalog);
if (debug.val) LOG.debug("Generating tab-completion keywords");
this.completer = new TokenCompletor(catalog);
this.reader.addCompletor(this.completer);
}
@Override
public void run() {
TerminalConnection tc = this.getClientConnection();
if (this.enable_csv == false) {
this.printHeader();
System.out.printf("Connected to %s:%d / Server Version: %s\n",
tc.hostname, tc.port, tc.client.getBuildString());
}
String query = "";
ClientResponse cresponse = null;
boolean stop = false;
try {
do {
try {
query = (this.enable_csv ? reader.readLine() : reader.readLine(PROMPT));
if (query == null || query.isEmpty()) continue;
query = query.trim();
// Check if the first token is one of our special keywords
String tokens[] = SPLITTER.split(query);
int retries = 3;
Command targetCmd = null;
boolean usage = false;
boolean reconnect = false;
while (retries-- > 0 && stop == false) {
// Check whether they want to execute a special command
for (Command c : Command.values()) {
if (tokens[0].equalsIgnoreCase(c.name())) {
targetCmd = c;
}
} // FOR
try {
if (targetCmd != null) {
switch (targetCmd) {
case EXEC:
// The second position should be the name of the procedure
// that they want to execute
if (tokens.length < 2) {
usage = true;
} else {
cresponse = this.execProcedure(tc.client, tokens[1], query, reconnect);
}
break;
case ENABLE:
this.processEnable(tc.client, tokens[1], query, reconnect);
break;
case QUIT:
stop = true;
break;
case DESCRIBE:
case SHOW:
throw new NotImplementedException("The command '" + targetCmd + "' is is not implemented");
default:
throw new RuntimeException("Unexpected command '" + targetCmd);
} // SWITCH
}
// Otherwise we'll send it to the server to deal with as an ad-hoc query
else {
cresponse = this.execQuery(tc.client, query);
}
} catch (NoConnectionsException ex) {
LOG.warn("Connection lost. Going to try to connect again...");
tc = this.getClientConnection();
reconnect = true;
continue;
}
break;
} // WHILE
// Just print out the result
if (cresponse != null) {
if (cresponse.getStatus() == Status.OK) {
System.out.println(this.formatResult(cresponse));
} else {
System.out.printf("Server Response: %s / %s\n",
cresponse.getStatus(),
cresponse.getStatusString());
}
}
// Print target command usage
else if (usage) {
assert(targetCmd != null);
System.out.print(setBoldText);
System.out.println("USAGE: " + targetCmd.name() + " " + targetCmd.usage);
System.out.print(setPlainText);
}
// Print warning if we're not supposed to stop
else if (stop == false && targetCmd != Command.ENABLE) {
LOG.warn("Return result is null");
}
// Fatal Error
} catch (RuntimeException ex) {
throw ex;
// Friendly Error
} catch (Exception ex) {
LOG.error(ex.getMessage());
Throwable cause = ex.getCause();
if (cause != null) {
LOG.error(cause.getMessage());
if (debug.val) cause.printStackTrace();
}
}
} while (query != null && stop == false);
} finally {
try {
if (tc != null) tc.client.close();
} catch (InterruptedException ex) {
// Ignore
}
}
}
private void printHeader() {
// System.out.print(setBoldText);
System.out.println(" _ _ ___ _____ ___ ___ ___");
System.out.println("| || |___/ __|_ _/ _ \\| _ \\ __|");
System.out.println("| __ |___\\__ \\ | || (_) | / _|");
System.out.println("|_||_| |___/ |_| \\___/|_|_\\___|");
System.out.println();
// System.out.println(setPlainText);
}
/**
* Get a client handle to a random site in the running cluster
* The return value includes what site the client connected to
* @return
*/
private TerminalConnection getClientConnection() {
String hostname = null;
int port = -1;
// Fixed hostname
if (this.hostname != null) {
if (this.hostname.contains(":")) {
String split[] = this.hostname.split("\\:", 2);
hostname = split[0];
port = Integer.valueOf(split[1]);
} else {
hostname = this.hostname;
port = this.port;
}
}
// Connect to random host and using a random port that it's listening on
else if (this.catalog != null) {
Site catalog_site = CollectionUtil.random(CatalogUtil.getAllSites(this.catalog));
hostname = catalog_site.getHost().getIpaddr();
port = catalog_site.getProc_port();
}
assert(hostname != null);
assert(port > 0);
if (debug.val)
LOG.debug(String.format("Creating new client connection to %s:%d",
hostname, port));
Client client = ClientFactory.createClient(128, null, false, null);
try {
client.createConnection(null, hostname, port, "user", "password");
} catch (Exception ex) {
String msg = String.format("Failed to connect to HStoreSite at %s:%d", hostname, port);
throw new RuntimeException(msg);
}
return new TerminalConnection(client, hostname, port);
}
/**
* Execute the given query as an ad-hoc request on the server and
* return the result.
* @param client
* @param query
* @return
* @throws Exception
*/
private ClientResponse execQuery(Client client, String query) throws Exception {
if (debug.val) LOG.debug("QUERY: " + query);
ClientResponse cresponse = client.callProcedure("@AdHoc", query);
return (cresponse);
}
/**
* Execute the given procedure on the server and return the result
* @param client
* @param procName
* @param query
* @return
* @throws Exception
*/
private ClientResponse execProcedure(Client client, String procName, String query, boolean reconnect) throws Exception {
Procedure catalog_proc = this.catalog_db.getProcedures().getIgnoreCase(procName);
if (catalog_proc == null) {
throw new Exception("Invalid stored procedure name '" + procName + "'");
}
// We now need to go through the rest of the parameters and convert them
// to proper type
Pattern p = Pattern.compile("^" + Command.EXEC.name() + "[ ]+" + procName + "[ ]+(.*?)[;]*", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(query);
List<Object> procParams = new ArrayList<Object>();
if (m.matches()) {
// Extract the parameters and then convert them to their appropriate type
List<String> params = HStoreTerminal.extractParams(m.group(1));
if (debug.val) LOG.debug("PARAMS: " + params);
if (params.size() != catalog_proc.getParameters().size()) {
String msg = String.format("Expected %d params for '%s' but %d parameters were given",
catalog_proc.getParameters().size(), catalog_proc.getName(), params.size());
throw new Exception(msg);
}
int i = 0;
for (ProcParameter catalog_param : catalog_proc.getParameters()) {
VoltType vtype = VoltType.get(catalog_param.getType());
Object value = VoltTypeUtil.getObjectFromString(vtype, params.get(i));
// HACK: Allow us to send one-element array parameters
if (catalog_param.getIsarray()) {
switch (vtype) {
case BOOLEAN:
value = new boolean[]{ (Boolean)value };
break;
case TINYINT:
case SMALLINT:
case INTEGER:
value = new int[]{ (Integer)value };
break;
case BIGINT:
value = new long[]{ (Long)value };
break;
case FLOAT:
case DECIMAL:
value = new double[]{ (Double)value };
break;
case STRING:
value = new String[]{ (String)value };
break;
case TIMESTAMP:
value = new TimestampType[]{ (TimestampType)value };
default:
assert(false);
} // SWITCH
}
procParams.add(value);
i++;
} // FOR
}
Object params[] = procParams.toArray();
if (this.enable_csv == false && reconnect == false) {
LOG.info(String.format("Executing transaction " + setBoldText + "%s(%s)" + setPlainText,
catalog_proc.getName(), StringUtil.toString(params, false, false)));
}
ClientResponse cresponse = client.callProcedure(catalog_proc.getName(), params);
return (cresponse);
}
protected void processEnable(Client client, String option, String query, boolean reconnect) throws Exception {
// HACK
this.enable_debug = true;
LOG.info("Enabled debug output");
}
/**
*
* @param paramStr
* @return
* @throws Exception
*/
protected static List<String> extractParams(String paramStr) throws Exception {
List<String> params = new ArrayList<String>();
int pos = -1;
int len = paramStr.length();
while (++pos < len) {
char cur = paramStr.charAt(pos);
// Skip if it's just a space
if (cur == ' ') continue;
// See if our current position is a quotation mark
// If it is, then we know that we have a string parameter
if (cur == '"') {
// Keep going until we reach an unescaped quotation mark
boolean escaped = false;
boolean valid = false;
StringBuilder sb = new StringBuilder();
while (++pos < len) {
cur = paramStr.charAt(pos);
if (cur == '\\') {
escaped = true;
} else if (cur == '"' && escaped == false) {
valid = true;
break;
} else {
escaped = false;
}
sb.append(cur);
} // WHILE
if (valid == false) {
throw new Exception("Invalid parameter string '" + sb + "'");
}
params.add(sb.toString());
// Otherwise just grab the substring to the next space
} else {
int next = paramStr.indexOf(" ", pos);
if (next == -1) {
params.add(paramStr.substring(pos));
pos = len;
} else {
params.add(paramStr.substring(pos, next));
pos = next;
}
}
}
return (params);
}
private String formatResult(ClientResponse cr) {
final VoltTable results[] = cr.getResults();
final int num_results = results.length;
StringBuilder sb = new StringBuilder();
if (this.enable_debug) {
sb.append(cr.toString());
}
else {
// MAIN BODY
if (this.enable_csv) {
StringWriter out = new StringWriter();
for (int i = 0; i < num_results; i++) {
if (i > 0) out.write("\n\n");
VoltTableUtil.csv(out, results[i], true);
} // FOR
sb.append(out.toString());
} else {
sb.append(VoltTableUtil.format(results));
}
// FOOTER
String footer = "";
if (this.enable_csv == false) {
if (num_results == 1) {
int row_count = results[0].getRowCount();
footer = String.format("%d row%s in set", row_count, (row_count > 1 ? "s" : ""));
}
else if (num_results == 0) {
footer = "No results returned";
}
else {
footer = num_results + " tables returned";
}
sb.append(String.format("\n%s (%.2f sec)\n", footer, (cr.getClientRoundtrip() / 1000d)));
}
}
return (sb.toString());
}
public static void main(String vargs[]) throws Exception {
ArgumentsParser args = ArgumentsParser.load(vargs,
ArgumentsParser.PARAM_CATALOG
);
HStoreTerminal term = new HStoreTerminal(args.catalog);
// CSV OUTPUT
if (args.hasBooleanParam(ArgumentsParser.PARAM_TERMINAL_CSV)) {
term.enable_csv = args.getBooleanParam(ArgumentsParser.PARAM_TERMINAL_CSV);
}
// HOSTNAME
if (args.hasParam(ArgumentsParser.PARAM_TERMINAL_HOST)) {
term.hostname = args.getParam(ArgumentsParser.PARAM_TERMINAL_HOST);
}
// PORT
if (args.hasParam(ArgumentsParser.PARAM_TERMINAL_PORT)) {
term.port = args.getIntParam(ArgumentsParser.PARAM_TERMINAL_PORT);
}
term.run();
}
}