package ring.commands.parser; import java.util.Arrays; import java.util.List; import java.util.ArrayList; import ring.commands.annotations.Form; import ring.commands.annotations.Scope; /** * The object representation of a command form. This class transforms * the information in the Form annotation into something useful. * @author projectmoon * */ public class CommandForm { private String id; private String clause; private List<CommandToken> tokens = new ArrayList<CommandToken>(); private Scope scope; private Scope cascadeType; public CommandForm(Form form) { try { parse(form); } catch (FormParsingException e) { throw new CommandException("There was a problem parsing a CommandForm.", e); } } public static List<CommandForm> processForms(Form ... forms) { List<CommandForm> ret = new ArrayList<CommandForm>(forms.length); for (Form form : forms) { ret.add(new CommandForm(form)); } return ret; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getClause() { return clause; } public void setClause(String clause) { this.clause = clause; } public Scope getScope() { return scope; } public void setScope(Scope scope) { this.scope = scope; } public void setCascadeType(Scope cascadeType) { if (cascadeType != Scope.LTR_CASCADING && cascadeType != Scope.RTL_CASCADING && cascadeType != Scope.NO_CASCADING) { throw new IllegalArgumentException("Invalid scope for cascade type. Must use RTL or LTR or NO."); } this.cascadeType = cascadeType; } public Scope getCascadeType() { return cascadeType; } public int getTokenLength() { return tokens.size(); } public List<CommandToken> getTokens() { return tokens; } public CommandToken getToken(int token) { return tokens.get(token); } public List<CommandToken> getDelimiters() { List<CommandToken> ret = new ArrayList<CommandToken>(); for (CommandToken token : getTokens()) { if (token.isDelimiter()) { ret.add(token); } } return ret; } public List<CommandToken> getVariables() { List<CommandToken> ret = new ArrayList<CommandToken>(); for (CommandToken token : getTokens()) { if (token.isVariable()) { ret.add(token); } } return ret; } public CommandToken getFirstVariable() { for (CommandToken token : tokens) { if (token.isVariable()) { return token; } } return null; } public CommandToken getLastVariable() { for (int c = tokens.size() - 1; c >= 0; c--) { CommandToken token = tokens.get(c); if (token.isVariable()) { return token; } } return null; } public boolean hasVariables() { for (CommandToken token : tokens) { if (token.isVariable()) { return true; } } return false; } public boolean hasBindableVariables() { for (CommandToken token : tokens) { if (token.isVariable() && !token.isText()) { return true; } } return false; } public boolean hasOnlyTextVariables(){ for (CommandToken token : tokens) { if (token.isVariable() && token.isText()) { return true; } } return false; } public String toString() { return getId(); } /** * Transform the given Form into a useful CommandForm object with * friendly properties and methods. * @param form */ private void parse(Form form) throws FormParsingException { setId(form.id()); setClause(form.clause()); setScope(form.scope()); String[] split = form.clause().split(" "); int c = 0; int count = 0; boolean foundScoped = false; for (String tokenString : split) { if (!tokenString.equals("")) { CommandToken token = new CommandToken(); token.setToken(tokenString); //The parser needs to keep track of if this is at the start. if (count == 0) { token.setAtStart(true); } //Determine if is variable or delimiter and handle accordingly. if (tokenString.startsWith(":") || tokenString.startsWith("$") || tokenString.startsWith("#")) { token.setVariable(true); try { Class<?>[] types = form.bind()[c].value(); List<Class<?>> bindTypes = Arrays.asList(types); token.setBindTypes(bindTypes); } catch (ArrayIndexOutOfBoundsException e) { throw new FormParsingException("Not all variables have bind types declared on form \"" + this + "\"."); } //Handle scoped variable specifically. Cascade detection is at the end of parsing. if (tokenString.startsWith("$")) { if (foundScoped) { throw new FormParsingException("There can only be one scoped variable in a command form."); } else { token.setScoped(true); token.setScope(form.scope()); foundScoped = true; } } else { token.setScoped(false); if (tokenString.startsWith("#")) { token.setText(true); } } c++; } else { token.setDelimiter(true); } tokens.add(token); count++; } } //Set atEnd property for the last command token. if (tokens.size() > 0) { tokens.get(tokens.size() - 1).setAtEnd(true); if (tokens.get(tokens.size() - 1).isDelimiter()) { throw new FormParsingException("Command form \"" + this + "\" cannot end with a delimiter."); } } //A bit more error checking if (this.hasBindableVariables() && !foundScoped) { throw new FormParsingException("Variable form \"" + this + "\" must have a scoped variable in the start or end position."); } //Figure out right vs left cascade. if (this.hasBindableVariables()) { detectCascade(); } else { noCascade(); } } /** * Detect and set which cascading mode to use for forms with variables in them. * The cascading mode determines how translation results are filtered through * the command chain. */ private void detectCascade() throws FormParsingException { if (this.hasVariables()) { CommandToken firstVariable = this.getFirstVariable(); CommandToken lastVariable = this.getLastVariable(); //Verification. if (firstVariable.isScoped() && lastVariable.isScoped() && (firstVariable != lastVariable)) { throw new FormParsingException("Cascade conflict. Only the first or last variable may be scoped, not both."); } if (!firstVariable.isScoped() && !lastVariable.isScoped()) { throw new FormParsingException("Variable form \"" + this + "\" does not have a scope variable in the start or end position."); } //Now we can cascade. if (firstVariable.isScoped()) { ltrCascade(); } else if (lastVariable.isScoped()) { rtlCascade(); } } } /** * Create a left-to-right cascade. */ private void ltrCascade() { CommandToken first = this.getFirstVariable(); for (CommandToken variable : this.getVariables()) { if (variable != first) { variable.setScope(Scope.LTR_CASCADING); } } setCascadeType(Scope.LTR_CASCADING); } /** * Create a right-to-left cascade. */ private void rtlCascade() { CommandToken last = this.getLastVariable(); for (CommandToken variable : this.getVariables()) { if (variable != last) { variable.setScope(Scope.RTL_CASCADING); } } setCascadeType(Scope.RTL_CASCADING); } private void noCascade() { for (CommandToken variable : this.getVariables()) { variable.setScope(Scope.NO_CASCADING); } setCascadeType(Scope.NO_CASCADING); } }