package ring.commands.parser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.python.core.PyException;
import org.python.core.PyObject;
import org.python.core.PyTuple;
import org.python.core.PyType;
import ring.commands.annotations.Form;
import ring.commands.annotations.Template;
/**
* The command parser receives a command template and parses commands given to it.
* It will match command input to a form of the command template. If no form matches,
* the parser will return null form its parse method. If more than one form matches,
* it will return the first form found.
* @author projectmoon
*
*/
public class CommandParser {
/**
* Lame helper class to return from the findForm method.
*
*/
private class CPTuple {
public CommandForm form;
public List<ParsedCommandToken> tokenList;
}
private String commandName;
private Command command;
private List<CommandForm> forms;
public CommandParser(Command command) throws CommandParsingException {
commandName = command.getCommandName();
this.command = command;
Template cmdTemplate = command.getClass().getAnnotation(Template.class);
if (cmdTemplate != null) {
initialize(cmdTemplate);
}
else {
throw new IllegalArgumentException("The Command object does not have a defined command Template!");
}
}
public CommandParser(PyObject pyCommand) throws CommandParsingException {
if (isInstanceOfCommand(pyCommand)) {
try {
Template cmdTemplate = (Template)pyCommand.__getattr__("__template__").__tojava__(Template.class);
if (cmdTemplate != null) {
initialize(cmdTemplate);
}
else {
throw new IllegalArgumentException("The Command object does not have a defined command Template!");
}
}
catch (PyException e) {
throw new IllegalArgumentException("The Command object does not have a defined command Template!");
}
}
else {
throw new IllegalArgumentException("The passed Python must be a class object derived from ring.commands.parser.Command");
}
}
/**
* Delegate to this method for actual object initialization.
* @param template
* @throws CommandParsingException
*/
private void initialize(Template template) throws CommandParsingException {
//Initial error checking.
HashSet<String> idCheck = new HashSet<String>();
for (Form form : template.value()) {
if (!idCheck.add(form.id())) {
throw new CommandParsingException("Supplied command template has one or more duplicate command form IDs.");
}
}
forms = CommandForm.processForms(template.value());
}
private boolean isInstanceOfCommand(PyObject pyObj) {
if (pyObj instanceof PyType) {
PyTuple mro = (PyTuple)pyObj.__getattr__("__mro__");
for (Object pyType : mro) {
if (pyType == Command.class) {
return true;
}
}
}
return false;
}
public Command getCommand() {
return command;
}
public List<CommandForm> getForms() {
return forms;
}
public String getCommandName() {
return commandName;
}
/**
* Parse the supplied command using the supplied CommandSender
* as the "origin point" for the command. This method will return
* a ParsedCommand object if the command parsing is successful.
* Otherwise, it will return null. Null is only returned if the
* command to be sent does not conform to the any of the forms
* of the Command object stored in this parser.
* @param sender
* @param command
* @return A ParsedCommand if parsing was successful, or null if it was unsuccessful.
*/
public ParsedCommand parse(CommandSender sender, String command) throws CommandParsingException {
String[] split = command.split(" ");
String clause = "";
//Do we even have a clause?
if (split.length > 1) {
clause = command.substring(command.indexOf(" ") + 1);
}
else {
clause = "";
}
//Figure out if the root command is actually correct.
if (!split[0].equalsIgnoreCase(commandName)) {
return null;
}
//Next find the correct command form.
CPTuple tuple = parseClause(clause);
if (tuple != null && tuple.form != null) {
ParsedCommand cmd = new ParsedCommand();
cmd.setFormID(tuple.form.getId());
cmd.setCommand(commandName);
cmd.setScope(tuple.form.getScope());
cmd.setCascadeType(tuple.form.getCascadeType());
//Delegate to ParsedCommand#initialize for object translation.
cmd.initialize(sender, tuple.tokenList);
return cmd;
}
else {
return null;
}
}
/**
* Parses the supplied command clause by searching for a suitable
* CommandForm and parsing it according to the grammar defined in said
* suitable CommandForm. The method returns a "tuple" object consisting
* of both the CommandForm and the list of parsed command tokens. If no
* suitable command form could be found, the method returns null.
* @param clause
* @return A tuple containing the CommandForm and parsed tokens, or null.
*/
private CPTuple parseClause(String clause) {
for (CommandForm form : forms) {
List<ParsedCommandToken> tokens = parseAndTestForm(form, clause);
if (tokens != null) {
CPTuple tuple = new CPTuple();
tuple.form = form;
tuple.tokenList = tokens;
return tuple;
}
}
return null;
}
/**
* This method parses and tests a CommandForm object to see if it lexically
* agrees with the supplied clause. The method actually delegates to three
* separate methods: one for handling no-token CommandForms, one for handling
* a single token CommandForm, and another for handling a multiple token CommandForm.
* @param form
* @param clause
* @return A list of parsed command tokens if the command matches, null otherwise.
*/
private List<ParsedCommandToken> parseAndTestForm(CommandForm form, String clause) {
//Special case for 1 token forms.
if (form.getTokenLength() == 1) {
return parseSingleTokenForm(form, clause);
}
else if (form.getTokenLength() > 1) {
return parseMultiTokenForm(form, clause);
}
else {
return parseNoTokenForm(form, clause);
}
}
/**
* Parse a no-token command form. This is the simplest CommandForm to deal with.
* @param form
* @param clause
* @return A list of parsed command tokens if the command matches, null otherwise.
*/
private List<ParsedCommandToken> parseNoTokenForm(CommandForm form, String clause) {
String[] split = clause.split(" ");
if (split.length == 1 && split[0].equals("")) {
return new ArrayList<ParsedCommandToken>(0);
}
else {
return null;
}
}
/**
* Method that tests to see if the supplied clause agrees with the supplied single-token
* CommandForm.
* @param form
* @param clause
* @return A list of parsed command tokens if the command matches, null otherwise.
*/
private List<ParsedCommandToken> parseSingleTokenForm(CommandForm form, String clause) {
String[] split = clause.split(" ");
CommandToken token = form.getToken(0);
if (token.isDelimiter() && split[0].equals(token.getToken()) && split.length == 1) {
return new ArrayList<ParsedCommandToken>(0);
}
else if (token.isVariable()) {
List<ParsedCommandToken> parsed = new ArrayList<ParsedCommandToken>(1);
ParsedCommandToken parsedToken = new ParsedCommandToken();
parsedToken.setStartIndex(0);
parsedToken.setEndIndex(split.length);
parsedToken.setToken(clause);
parsedToken.setMatched(token);
parsed.add(parsedToken);
return parsed;
}
else {
return null;
}
}
/**
* Method that parses the supplied multiple token CommandForm to see if the
* supplied clause agrees with it. This method is rather complex, and also
* delegates to a finishing method in a vain attempt to be readable.
* @param form
* @param clause
* @return A list of parsed command tokens if the clause matches, null otherwise.
*/
private List<ParsedCommandToken> parseMultiTokenForm(CommandForm form, String clause) {
String[] split = clause.split(" ");
List<ParsedCommandToken> parsed = new ArrayList<ParsedCommandToken>();
String prevDelim = null;
ParsedCommandToken currToken;
ParsedCommandToken prevToken = null;
List<CommandToken> delims = form.getDelimiters();
int c = 0;
boolean errors = false;
if (delims.size() == 0) errors = true;
for (CommandToken delim : delims) {
String currDelim = delim.getToken();
currToken = new ParsedCommandToken();
currToken.setParentClause(split);
currToken.setStartIndex(c);
parsed.add(currToken);
boolean found = false;
for (; c < split.length; c++) {
//Error check if we start with a delimiter.
if (delim.isAtStart() && delim.isDelimiter() && c == 0) {
if (!delim.getToken().equals(split[c])) {
errors = true;
break;
}
}
//Find the start and end indices of the matched token.
if (!split[c].equals(currDelim)) {
//Make sure we don't slip up on delims and arguments that get mixed.
//We must rewind and fix the previous token if we find this case.
if (prevDelim != null && split[c].equals(prevDelim)) {
prevToken.setEndIndex(c);
currToken.setStartIndex(c + 1);
}
}
else {
currToken.setEndIndex(c);
found = true;
break;
}
}
if (!found) {
errors = true;
}
if (errors) {
break;
}
prevDelim = currDelim;
prevToken = currToken;
}
if (!errors) {
//convert remainder of tokens to ParsedCommandToken
//Forms that end in a variable need c advanced one more space or they will
//get the last delimiter too.
if (form.getToken(form.getTokenLength() - 1).isVariable()) {
c++;
}
ParsedCommandToken lastToken = new ParsedCommandToken(c, split.length);
parsed.add(lastToken);
parsed = finishMultiTokenParsing(form, parsed, split);
return parsed;
}
else {
return null;
}
}
/**
* Final parsing step to craft the variable names, remove blank tokens, and
* match the parsed token to the variable names they map to during multitoken
* command form parsing.
* @param form
* @param parsed
* @param split
* @return The polished list of parsed command tokens, should the list have anything in it. Null otherwise.
*/
private List<ParsedCommandToken> finishMultiTokenParsing(CommandForm form, List<ParsedCommandToken> parsed, String[] split) {
//Craft the tokens.
for (ParsedCommandToken token : parsed) {
craftParsedToken(token, split);
}
//Remove all blanks.
List<ParsedCommandToken> blanks = new ArrayList<ParsedCommandToken>();
for (ParsedCommandToken token : parsed) {
if (token.getToken().trim().equals("")) {
blanks.add(token);
}
}
parsed.removeAll(blanks);
if (parsed.size() <= 0) {
return null;
}
else {
//Set the matched tokens.
//Length and order of variable list should that of parsed list.
List<CommandToken> variables = form.getVariables();
try {
for (int c = 0; c < variables.size(); c++) {
parsed.get(c).setMatched(variables.get(c));
}
return parsed;
}
catch (IndexOutOfBoundsException e) {
return null;
}
}
}
/**
* Helper method that sticks the extracted text of the parsed command token into
* its token property.
* @param token
* @param clause
*/
private void craftParsedToken(ParsedCommandToken token, String[] clause) {
String text = "";
for (int c = token.getStartIndex(); c < token.getEndIndex(); c++) {
text += clause[c] + " ";
}
token.setToken(text.trim());
}
}