package edu.brown.terminal;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Pattern;
import jline.Completor;
import org.apache.log4j.Logger;
import org.hsqldb.Tokens;
import org.voltdb.catalog.Catalog;
import org.voltdb.catalog.Column;
import org.voltdb.catalog.Database;
import org.voltdb.catalog.Procedure;
import org.voltdb.catalog.Table;
import org.voltdb.types.QueryType;
import edu.brown.catalog.CatalogUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
import edu.brown.utils.CollectionUtil;
import edu.brown.utils.StringUtil;
/**
* This is a class that holds a list of the table names for an instance of HStore
* @author gen
* @author pavlo
*/
public class TokenCompletor implements Completor {
private static final Logger LOG = Logger.getLogger(TokenCompletor.class);
private static final LoggerBoolean debug = new LoggerBoolean();
protected static final String DELIMITER = " ";
protected static final Pattern SPLIT = Pattern.compile("[" + DELIMITER + "]+");
private static final String TABLE_PREFIXES[] = {
"UPDATE",
"FROM",
"DESCRIBE"
};
private static final String COLUMN_PREFIXES[] = {
"WHERE",
"SET"
};
private static final String PROC_PREFIXES[] = {
"EXEC"
};
final Catalog catalog;
final SortedSet<String> emptyTokens = new TreeSet<String>();
final SortedSet<String> allTokens = new TreeSet<String>();
final SortedSet<String> sqlTokens = new TreeSet<String>();
final SortedSet<String> specialTokens = new TreeSet<String>();
final SortedSet<String> commandTokens = new TreeSet<String>();
final SortedSet<String> tableTokens = new TreeSet<String>();
final SortedSet<String> columnTokens = new TreeSet<String>();
final SortedSet<String> procTokens = new TreeSet<String>();
final Set<String> tablePrefixes = new HashSet<String>();
final Set<String> columnPrefixes = new HashSet<String>();
final Set<String> procPrefixes = new HashSet<String>();
final Map<String, String> tokenCaseSensitive = new HashMap<String, String>();
public TokenCompletor(Catalog catalog) throws Exception {
this.catalog = catalog;
Database catalog_db = CatalogUtil.getDatabase(catalog);
// Core SQL Reserved Words from HSQLDB
for (int i = 0; i < 1000; i++) {
if (Tokens.isCoreKeyword(i)) {
String keyword = Tokens.getKeyword(i);
if (keyword != null) this.sqlTokens.add(keyword);
}
} // FOR
// Special command tokens
for (QueryType qtype : QueryType.values()) {
if (qtype == QueryType.INVALID || qtype == QueryType.NOOP) continue;
this.commandTokens.add(qtype.name());
} // FOR
for (HStoreTerminal.Command c : HStoreTerminal.Command.values()) {
this.commandTokens.add(c.name());
this.specialTokens.add(c.name());
} // FOR
// Catalog Keywords
// Tables, columns, procedures names
CollectionUtil.addAll(this.tablePrefixes, TABLE_PREFIXES);
CollectionUtil.addAll(this.columnPrefixes, COLUMN_PREFIXES);
for (Table catalog_tbl : CatalogUtil.getDataTables(catalog_db)) {
this.tableTokens.add(catalog_tbl.getName());
this.columnPrefixes.add(catalog_tbl.getName() + ".");
for (Column catalog_col : catalog_tbl.getColumns()) {
this.columnTokens.add(catalog_col.getName().toUpperCase());
} // FOR
} // FOR
CollectionUtil.addAll(this.procPrefixes, PROC_PREFIXES);
for (Procedure catalog_proc : catalog_db.getProcedures()) {
String procName = catalog_proc.getName().toUpperCase();
this.tokenCaseSensitive.put(procName, catalog_proc.getName());
this.procTokens.add(procName);
} // FOR
this.allTokens.addAll(this.sqlTokens);
this.allTokens.addAll(this.tableTokens);
this.allTokens.addAll(this.columnTokens);
this.allTokens.addAll(this.procTokens);
if (debug.val)
LOG.debug("Token Information:\n" + this.toString());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public int complete(String buffer, int cursor, List clist) {
if (buffer == null || buffer.isEmpty()) {
clist.addAll(this.commandTokens);
return (0);
}
// Find the current token up to the previous space
int lastIndex = buffer.lastIndexOf(DELIMITER);
String tokens[] = SPLIT.split(buffer);
String last = tokens[tokens.length - 1].trim().toUpperCase();
if (debug.val) {
LOG.debug("BUFFER: '" + buffer + "'");
LOG.debug("CURSOR: " + cursor);
LOG.debug("TOKENS: " + Arrays.toString(tokens));
}
// Figure out whether they are trying to enter in a SQL
// token, or a name of an element in the database
String prevToken = null;
if (buffer.endsWith(DELIMITER) || tokens.length == 1) {
prevToken = last;
} else {
prevToken = tokens[tokens.length-2].toUpperCase();
}
prevToken = prevToken.trim().toUpperCase();
if (debug.val) LOG.debug("PREV: " + prevToken);
// The first token must always be a SQL command token
// Otherwise, check which candidates to use based on
// what's in the previous token
SortedSet<String> candidates = (tokens.length == 1 ? this.commandTokens : this.sqlTokens);
// Check whether the buffer is "EXEC <ProcedureName>"
// If it is, then we don't want to return any tokens for now
// Ideally we want to be able to print the parameter info for this procedure
if (tokens.length >= 2 &&
tokens[0].equalsIgnoreCase(HStoreTerminal.Command.EXEC.name()) &&
this.procTokens.contains(tokens[1].toUpperCase())) {
if (debug.val) LOG.debug("EXEC COMMAND: '" + buffer + "'");
candidates = this.emptyTokens;
}
// TABLE
else if (this.tablePrefixes.contains(prevToken)) {
if (debug.val) LOG.debug("TABLE PREFIX: '" + prevToken + "'");
candidates = this.tableTokens;
}
// COLUMN
else if (this.columnPrefixes.contains(prevToken)) {
if (debug.val) LOG.debug("COLUMN PREFIX: " + prevToken);
candidates = this.columnTokens;
}
// PROCEDURE
else if (this.procPrefixes.contains(prevToken)) {
if (debug.val) LOG.debug("PROC PREFIX: " + prevToken);
candidates = this.procTokens;
} else if (debug.val) {
LOG.debug("Using all candidates!");
}
assert(candidates != null);
if (debug.val) LOG.debug("CANDIDATES: " + candidates);
if (buffer.endsWith(DELIMITER)) {
for (String can : candidates) {
if (this.tokenCaseSensitive.containsKey(can)) {
can = this.tokenCaseSensitive.get(can);
}
clist.add(can);
} // FOR
} else {
Collection<String> matches = candidates.tailSet(last);
if (debug.val) LOG.debug("MATCHES: " + matches);
for (String can : matches) {
if (debug.val) LOG.debug("Candidate: " + can);
if (can.startsWith(last) == false || can.equalsIgnoreCase(last)) {
if (debug.val) LOG.debug("Invalid match!");
break;
}
// Fix capitalization
if (this.tokenCaseSensitive.containsKey(can)) {
can = this.tokenCaseSensitive.get(can);
}
int index = can.indexOf(DELIMITER, cursor);
if (index != -1) {
can = can.substring(0, index + 1);
}
clist.add(can);
} // FOR
}
if (clist.size() == 1) {
clist.set(0, ((String) clist.get(0)) + " ");
}
return (clist.size() == 0 ? -1 : lastIndex+1);
}
@Override
public String toString() {
Class<?> clazz = this.getClass();
Map<String, Object> m = new LinkedHashMap<String, Object>();
for (Field f : clazz.getDeclaredFields()) {
if (f.getName().equalsIgnoreCase("catalog")) continue;
if (f.getName().equalsIgnoreCase("allTokens")) continue;
Object obj = null;
try {
obj = f.get(this);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
m.put(f.getName(), obj);
} // FOR
return StringUtil.formatMaps(m);
}
}