package org.basex.core; import static org.basex.core.Text.*; import static org.basex.util.Token.*; import java.util.Locale; import org.basex.core.Commands.Cmd; import org.basex.core.Commands.CmdAlter; import org.basex.core.Commands.CmdCreate; import org.basex.core.Commands.CmdDrop; import org.basex.core.Commands.CmdIndex; import org.basex.core.Commands.CmdIndexInfo; import org.basex.core.Commands.CmdInfo; import org.basex.core.Commands.CmdOptimize; import org.basex.core.Commands.CmdPerm; import org.basex.core.Commands.CmdRepo; import org.basex.core.Commands.CmdShow; import org.basex.core.cmd.Add; import org.basex.core.cmd.AlterDB; import org.basex.core.cmd.AlterUser; import org.basex.core.cmd.Check; import org.basex.core.cmd.Close; import org.basex.core.cmd.Copy; import org.basex.core.cmd.CreateBackup; import org.basex.core.cmd.CreateDB; import org.basex.core.cmd.CreateEvent; import org.basex.core.cmd.CreateIndex; import org.basex.core.cmd.CreateUser; import org.basex.core.cmd.Cs; import org.basex.core.cmd.Delete; import org.basex.core.cmd.DropBackup; import org.basex.core.cmd.DropDB; import org.basex.core.cmd.DropEvent; import org.basex.core.cmd.DropIndex; import org.basex.core.cmd.DropUser; import org.basex.core.cmd.Exit; import org.basex.core.cmd.Export; import org.basex.core.cmd.Find; import org.basex.core.cmd.Flush; import org.basex.core.cmd.Get; import org.basex.core.cmd.Grant; import org.basex.core.cmd.Help; import org.basex.core.cmd.Info; import org.basex.core.cmd.InfoDB; import org.basex.core.cmd.InfoIndex; import org.basex.core.cmd.InfoStorage; import org.basex.core.cmd.Kill; import org.basex.core.cmd.List; import org.basex.core.cmd.ListDB; import org.basex.core.cmd.Open; import org.basex.core.cmd.Optimize; import org.basex.core.cmd.OptimizeAll; import org.basex.core.cmd.Password; import org.basex.core.cmd.Rename; import org.basex.core.cmd.Replace; import org.basex.core.cmd.RepoDelete; import org.basex.core.cmd.RepoInstall; import org.basex.core.cmd.RepoList; import org.basex.core.cmd.Restore; import org.basex.core.cmd.Retrieve; import org.basex.core.cmd.Run; import org.basex.core.cmd.Set; import org.basex.core.cmd.ShowBackups; import org.basex.core.cmd.ShowDatabases; import org.basex.core.cmd.ShowEvents; import org.basex.core.cmd.ShowSessions; import org.basex.core.cmd.ShowUsers; import org.basex.core.cmd.Store; import org.basex.core.cmd.XQuery; import org.basex.io.IOFile; import org.basex.query.QueryContext; import org.basex.query.QueryException; import org.basex.query.QueryParser; import org.basex.query.item.QNm; import org.basex.util.Array; import org.basex.util.InputInfo; import org.basex.util.InputParser; import org.basex.util.Levenshtein; import org.basex.util.Util; import org.basex.util.list.StringList; /** * This is a parser for command strings, creating {@link Command} instances. * Several commands can be formulated in one string and separated by semicolons. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public final class CommandParser extends InputParser { /** Context. */ private final Context ctx; /** Password reader. */ private PasswordReader passwords; /** Suggest possible completions. */ private boolean suggest; /** * Constructor, parsing the input queries. * @param in query input * @param c context */ public CommandParser(final String in, final Context c) { super(in); ctx = c; } /** * Attaches a password reader. * @param pr password reader * @return self reference */ public CommandParser password(final PasswordReader pr) { passwords = pr; return this; } /** * Parses the input as single command and returns the result. * @return command * @throws QueryException query exception */ public Command parseSingle() throws QueryException { final Cmd cmd = consume(Cmd.class, null); final Command command = parse(cmd, true); consumeWS(); if(more()) throw help(null, cmd); return command; } /** * Parses the input and returns a command list. * @return commands * @throws QueryException query exception */ public Command[] parse() throws QueryException { Command[] list = new Command[0]; while(true) { final Cmd cmd = consume(Cmd.class, null); list = Array.add(list, parse(cmd, false)); consumeWS(); if(!more()) return list; if(!consume(';')) throw help(null, cmd); } } /** * Parses the input and returns a command list. * @param s suggest flag * @return commands * @throws QueryException query exception */ public Command[] parse(final boolean s) throws QueryException { suggest = s; return parse(); } /** * Parses a single command. * @param cmd command definition * @param s single command expected * @return resulting command * @throws QueryException query exception */ private Command parse(final Cmd cmd, final boolean s) throws QueryException { switch(cmd) { case CREATE: switch(consume(CmdCreate.class, cmd)) { case BACKUP: return new CreateBackup(glob(cmd)); case DATABASE: case DB: return new CreateDB(name(cmd), s ? remaining(null) : string(null)); case INDEX: return new CreateIndex(consume(CmdIndex.class, cmd)); case USER: return new CreateUser(name(cmd), password()); case EVENT: return new CreateEvent(name(cmd)); } break; case COPY: return new Copy(name(cmd), name(cmd)); case ALTER: switch(consume(CmdAlter.class, cmd)) { case DATABASE: case DB: return new AlterDB(name(cmd), name(cmd)); case USER: return new AlterUser(name(cmd), password()); } break; case OPEN: return new Open(string(cmd)); case CHECK: return new Check(string(cmd)); case ADD: String arg = key(TO, null) ? string(cmd) : null; return new Add(arg, s ? remaining(cmd) : string(cmd)); case STORE: arg = key(TO, null) ? string(cmd) : null; return new Store(arg, s ? remaining(cmd) : string(cmd)); case RETRIEVE: return new Retrieve(string(cmd)); case DELETE: return new Delete(string(cmd)); case RENAME: return new Rename(string(cmd), string(cmd)); case REPLACE: return new Replace(string(cmd), string(cmd)); case INFO: switch(consume(CmdInfo.class, cmd)) { case NULL: return new Info(); case DATABASE: case DB: return new InfoDB(); case INDEX: return new InfoIndex(consume(CmdIndexInfo.class, null)); case STORAGE: String arg1 = number(null); final String arg2 = arg1 != null ? number(null) : null; if(arg1 == null) arg1 = xquery(null); return new InfoStorage(arg1, arg2); } break; case CLOSE: return new Close(); case LIST: final String input = string(null); return input == null ? new List() : new ListDB(input); case DROP: switch(consume(CmdDrop.class, cmd)) { case DATABASE: case DB: return new DropDB(glob(cmd)); case INDEX: return new DropIndex(consume(CmdIndex.class, cmd)); case USER: return new DropUser(glob(cmd), key(ON, null) ? glob(cmd) : null); case BACKUP: return new DropBackup(glob(cmd)); case EVENT: return new DropEvent(name(cmd)); } break; case OPTIMIZE: switch(consume(CmdOptimize.class, cmd)) { case NULL: return new Optimize(); case ALL: return new OptimizeAll(); } break; case EXPORT: return new Export(string(cmd)); case XQUERY: return new XQuery(xquery(cmd)); case RUN: return new Run(string(cmd)); case FIND: return new Find(string(cmd)); case CS: return new Cs(xquery(cmd)); case GET: return new Get(name(cmd)); case SET: return new Set(name(cmd), string(null)); case PASSWORD: return new Password(password()); case HELP: String hc = name(null); String form = null; if(hc != null) { if(hc.equalsIgnoreCase("wiki")) { form = hc; hc = null; } else { qp = qm; hc = consume(Cmd.class, cmd).toString(); form = name(null); } } return new Help(hc, form); case EXIT: return new Exit(); case FLUSH: return new Flush(); case KILL: return new Kill(string(cmd)); case RESTORE: return new Restore(name(cmd)); case SHOW: switch(consume(CmdShow.class, cmd)) { case DATABASES: return new ShowDatabases(); case SESSIONS: return new ShowSessions(); case USERS: return new ShowUsers(key(ON, null) ? name(cmd) : null); case BACKUPS: return new ShowBackups(); case EVENTS: return new ShowEvents(); default: } break; case GRANT: final CmdPerm perm = consume(CmdPerm.class, cmd); if(perm == null) throw help(null, cmd); final String db = key(ON, null) ? glob(cmd) : null; key(TO, cmd); return new Grant(perm, glob(cmd), db); case REPO: switch(consume(CmdRepo.class, cmd)) { case INSTALL: return new RepoInstall(string(cmd), new InputInfo(this)); case DELETE: return new RepoDelete(string(cmd), new InputInfo(this)); case LIST: return new RepoList(); default: } break; default: } throw Util.notexpected("command specified, but not implemented yet"); } /** * Parses and returns a string, delimited by a space or semicolon. * Quotes can be used to include spaces. * @param cmd referring command; if specified, the result must not be empty * @return string * @throws QueryException query exception */ String string(final Cmd cmd) throws QueryException { final StringBuilder sb = new StringBuilder(); consumeWS(); boolean q = false; while(more()) { final char c = curr(); if(!q && (c <= ' ' || c == ';')) break; if(c == '"') q ^= true; else sb.append(c); consume(); } return finish(cmd, sb); } /** * Parses and returns the remaining string. Quotes at the beginning and end * of the argument will be stripped. * @param cmd referring command; if specified, the result must not be empty * @return remaining string * @throws QueryException query exception */ private String remaining(final Cmd cmd) throws QueryException { final StringBuilder sb = new StringBuilder(); consumeWS(); while(more()) sb.append(consume()); String arg = finish(cmd, sb); if(arg != null) { // chop quotes; substrings are faster than replaces... if(arg.startsWith("\"")) arg = arg.substring(1); if(arg.endsWith("\"")) arg = arg.substring(0, arg.length() - 1); } return arg; } /** * Parses and returns an xquery expression. * @param cmd referring command; if specified, the result must not be empty * @return path * @throws QueryException query exception */ private String xquery(final Cmd cmd) throws QueryException { consumeWS(); final StringBuilder sb = new StringBuilder(); if(more() && !curr(';')) { final QueryParser p = new QueryParser(query, new QueryContext(ctx)); p.qp = qp; p.parse(null); sb.append(query.substring(qp, p.qp)); qp = p.qp; } return finish(cmd, sb); } /** * Parses and returns a name. A name is limited to letters, digits, * underscores, dashes, and periods: {@code [A-Za-z0-9_-]+}. * @param cmd referring command; if specified, the result must not be empty * @return name * @throws QueryException query exception */ private String name(final Cmd cmd) throws QueryException { consumeWS(); final StringBuilder sb = new StringBuilder(); while(letterOrDigit(curr()) || curr('-')) sb.append(consume()); return finish(cmd, !more() || curr(';') || ws(curr()) ? sb : null); } /** * Parses and returns a password string. * @return password string * @throws QueryException query exception */ String password() throws QueryException { final String pw = string(null); return pw != null ? pw : passwords == null ? "" : passwords.password(); } /** * Parses and returns a glob expression, which extends the {@link #name} * with asterisks, question marks and commands. See {@link IOFile#regex} * for more details. * @param cmd referring command; if specified, the result must not be empty * @return glob expression * @throws QueryException query exception */ private String glob(final Cmd cmd) throws QueryException { consumeWS(); final StringBuilder sb = new StringBuilder(); while(true) { final char c = curr(); if(!letterOrDigit(c) && c != '-' && c != '*' && c != '?' && c != ',') { return finish(cmd, !more() || curr(';') || ws(curr()) ? sb : null); } sb.append(consume()); } } /** * Parses and returns the specified keyword. * @param key token to be parsed * @param cmd referring command; if specified, the keyword is mandatory * @return result of check * @throws QueryException query exception */ private boolean key(final String key, final Cmd cmd) throws QueryException { consumeWS(); final int p = qp; final boolean ok = (consume(key) || consume(key.toLowerCase(Locale.ENGLISH))) && (curr(0) || ws(curr())); if(!ok) { qp = p; if(cmd != null) throw help(null, cmd); } return ok; } /** * Parses and returns a string result. * @param cmd referring command; if specified, the result must not be empty * @param s input string, or {@code null} if invalid * @return string result, or {@code null} * @throws QueryException query exception */ private String finish(final Cmd cmd, final StringBuilder s) throws QueryException { if(s != null && s.length() != 0) return s.toString(); if(cmd != null) throw help(null, cmd); return null; } /** * Parses and returns a number. * @param cmd referring command; if specified, the result must not be empty * @return name * @throws QueryException query exception */ private String number(final Cmd cmd) throws QueryException { consumeWS(); final StringBuilder sb = new StringBuilder(); if(curr() == '-') sb.append(consume()); while(digit(curr())) sb.append(consume()); return finish(cmd, !more() || curr(';') || ws(curr()) ? sb : null); } /** * Consumes all whitespace characters from the beginning of the remaining * query. */ private void consumeWS() { while(qp < ql && query.charAt(qp) <= ' ') ++qp; qm = qp - 1; } /** * Returns the index of the found string or throws an error. * @param cmp possible completions * @param par parent command * @param <E> token type * @return index * @throws QueryException query exception */ private <E extends Enum<E>> E consume(final Class<E> cmp, final Cmd par) throws QueryException { final String token = name(null); if(!(suggest && token != null && token.length() <= 1)) { try { // return command reference; allow empty strings as input ("NULL") final String t = token == null ? "NULL" : token.toUpperCase(Locale.ENGLISH); return Enum.valueOf(cmp, t); } catch(final IllegalArgumentException ex) { /* will not happen. */ } } final Enum<?>[] alt = list(cmp, token); if(token == null) { // show command error or available command extensions throw par == null ? error(list(alt), EXPECTING_CMD) : help(list(alt), par); } // output error for similar commands final byte[] name = lc(token(token)); final Levenshtein ls = new Levenshtein(); for(final Enum<?> s : list(cmp, null)) { final byte[] sm = lc(token(s.name().toLowerCase(Locale.ENGLISH))); if(ls.similar(name, sm, 0) && Cmd.class.isInstance(s)) throw error(list(alt), UNKNOWN_SIMILAR_X, name, sm); } // show unknown command error or available command extensions throw par == null ? error(list(alt), UNKNOWN_TRY_X, token) : help(list(alt), par); } /** * Returns help output as query exception instance. * Prints some command info. * @param alt input alternatives * @param cmd input completions * @return QueryException query exception */ private QueryException help(final StringList alt, final Cmd cmd) { return error(alt, SYNTAX_X, cmd.help(true, false)); } /** * Returns the command list. * @param <T> token type * @param en enumeration * @param i user input * @return completions */ private static <T extends Enum<T>> Enum<?>[] list( final Class<T> en, final String i) { Enum<?>[] list = new Enum<?>[0]; final String t = i == null ? "" : i.toUpperCase(Locale.ENGLISH); for(final Enum<?> e : en.getEnumConstants()) { if(e.name().startsWith(t)) { final int s = list.length; final Enum<?>[] tmp = new Enum<?>[s + 1]; System.arraycopy(list, 0, tmp, 0, s); tmp[s] = e; list = tmp; } } return list; } /** * Returns a query exception instance. * @param comp input completions * @param m message * @param e extension * @return query exception */ private QueryException error(final StringList comp, final String m, final Object... e) { return new QueryException(input(), new QNm(), m, e).suggest(this, comp); } /** * Converts the specified commands into a string list. * @param comp input completions * @return string list */ private static StringList list(final Enum<?>[] comp) { final StringList list = new StringList(); for(final Enum<?> c : comp) list.add(c.name().toLowerCase(Locale.ENGLISH)); return list; } }