/**
*
*/
package org.sinnlabs.dbvim.evaluator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.sinnlabs.dbvim.db.Value;
import org.sinnlabs.dbvim.db.model.DBField;
import org.sinnlabs.dbvim.evaluator.exceptions.ParseException;
import org.sinnlabs.dbvim.form.FormFieldResolver;
import org.sinnlabs.dbvim.ui.IField;
/**
* An evaluator that is able to evaluate boolean expressions on field values.
* @author peter.liverovsky
*
*/
public class DatabaseConditionBuilder {
private final ConditionTokenizer tokenizer;
private final Map<String, Function> functions;
private final Map<String, List<Operator>> operators;
private final Map<String, Constant> constants;
private final String functionArgumentSeparator;
private final Map<String, BracketPair> functionBrackets;
private final Map<String, BracketPair> expressionBrackets;
/** A constant that represents NULL value */
public static final Constant NULL = new Constant("$NULL$");
/** The negate unary operator in the standard operator precedence.*/
public static final Operator NEGATE = new Operator("-", 1, Operator.Associativity.RIGHT, 5, "-");
/** The negate unary operator in the Excel like operator precedence.*/
public static final Operator NEGATE_HIGH = new Operator("-", 1, Operator.Associativity.RIGHT, 7, "-");
/** The substraction operator.*/
public static final Operator MINUS = new Operator("-", 2, Operator.Associativity.LEFT, 3, "-");
/** The addition operator.*/
public static final Operator PLUS = new Operator("+", 2, Operator.Associativity.LEFT, 3, "+");
/** The multiplication operator.*/
public static final Operator MULTIPLY = new Operator("*", 2, Operator.Associativity.LEFT, 4, "*");
/** The division operator.*/
public static final Operator DIVIDE = new Operator("/", 2, Operator.Associativity.LEFT, 4, "/");
/** The <a href="http://en.wikipedia.org/wiki/Modulo_operation">modulo operator</a>.*/
public static final Operator MODULO = new Operator("%", 2, Operator.Associativity.LEFT, 4, "%");
/** The equality operator */
public static final Operator EQ = new Operator("=", 2, Operator.Associativity.LEFT, 2, "=");
/** The not equality operator */
public static final Operator NOT_EQ = new Operator("!=", 2, Operator.Associativity.LEFT, 2, "<>");
/** The grate then operator */
public static final Operator GT = new Operator(">", 2, Operator.Associativity.LEFT, 2, ">");
/** The grate or equal operator */
public static final Operator GE = new Operator(">=", 2, Operator.Associativity.LEFT, 2, ">=");
/** The less then operator */
public static final Operator LT = new Operator("<", 2, Operator.Associativity.LEFT, 2, "<");
/** The less or equal operator */
public static final Operator LE = new Operator("<=", 2, Operator.Associativity.LEFT, 2, "<=");
/** The LIKE operator */
public static final Operator LIKE = new Operator("LIKE", 2, Operator.Associativity.LEFT, 2, "LIKE");
/** The logical AND operator */
public static final Operator AND = new Operator("AND", 2, Operator.Associativity.LEFT, 1, "AND");
/** The logical OR operator */
public static final Operator OR = new Operator("OR", 2, Operator.Associativity.LEFT, 1, "OR");
/** The logical NOT operator */
public static final Operator NOT = new Operator("NOT", 1, Operator.Associativity.RIGHT, 2, "NOT");
/** The standard whole set of predefined operators */
private static final Operator[] OPERATORS = new Operator[]{NEGATE, MINUS, PLUS, MULTIPLY,
DIVIDE, MODULO, EQ, NOT_EQ, GT, GE, LT, LE, LIKE, AND, OR, NOT};
/** The whole set of predefined constants */
private static final Constant[] CONSTANTS = new Constant[]{NULL};
private static Parameters DEFAULT_PARAMETERS;
private static Parameters getParameters() {
if (DEFAULT_PARAMETERS == null) {
DEFAULT_PARAMETERS = getDefaultParameters();
}
return DEFAULT_PARAMETERS;
}
/** Gets a copy of DoubleEvaluator default parameters.
* <br>The returned parameters contains all the predefined operators, functions and constants.
* <br>Each call to this method create a new instance of Parameters.
* @return a Paramaters instance
*/
private static Parameters getDefaultParameters() {
Parameters result = new Parameters();
result.addOperators(Arrays.asList(OPERATORS));
//result.addFunctions(Arrays.asList(FUNCTIONS));
result.addConstants(Arrays.asList(CONSTANTS));
result.addFunctionBracket(BracketPair.PARENTHESES);
result.addExpressionBracket(BracketPair.PARENTHESES);
return result;
}
public DatabaseConditionBuilder() {
Parameters parameters = getParameters();
final ArrayList<String> tokenDelimitersBuilder = new ArrayList<String>();
this.functions = new HashMap<String, Function>();
this.operators = new HashMap<String, List<Operator>>();
this.constants = new HashMap<String, Constant>();
this.functionBrackets = new HashMap<String, BracketPair>();
for (final BracketPair pair : parameters.getFunctionBrackets()) {
functionBrackets.put(pair.getOpen(), pair);
functionBrackets.put(pair.getClose(), pair);
tokenDelimitersBuilder.add(pair.getOpen());
tokenDelimitersBuilder.add(pair.getClose());
}
this.expressionBrackets = new HashMap<String, BracketPair>();
for (final BracketPair pair : parameters.getExpressionBrackets()) {
expressionBrackets.put(pair.getOpen(), pair);
expressionBrackets.put(pair.getClose(), pair);
tokenDelimitersBuilder.add(pair.getOpen());
tokenDelimitersBuilder.add(pair.getClose());
}
if (operators!=null) {
for (Operator ope : parameters.getOperators()) {
tokenDelimitersBuilder.add(ope.getSymbol());
List<Operator> known = this.operators.get(ope.getSymbol());
if (known==null) {
known = new ArrayList<Operator>();
this.operators.put(ope.getSymbol(), known);
}
known.add(ope);
if (known.size()>1) {
validateHomonyms(known);
}
}
}
boolean needFunctionSeparator = false;
if (parameters.getFunctions()!=null) {
for (Function function : parameters.getFunctions()) {
this.functions.put(parameters.getTranslation(function.getName()), function);
if (function.getMaximumArgumentCount()>1) {
needFunctionSeparator = true;
}
}
}
if (parameters.getConstants()!=null) {
for (Constant constant : parameters.getConstants()) {
this.constants.put(parameters.getTranslation(constant.getName()), constant);
}
}
functionArgumentSeparator = parameters.getFunctionArgumentSeparator();
if (needFunctionSeparator) {
tokenDelimitersBuilder.add(functionArgumentSeparator);
}
tokenizer = new ConditionTokenizer('.', tokenDelimitersBuilder);
}
/** Validates that homonym operators are valid.
* <br>Homonym operators are operators with the same name (like the unary - and the binary - operators)
* <br>This method is called when homonyms are passed to the constructor.
* <br>This default implementation only allows the case where there's two operators, one binary and one unary.
* Subclasses can override this method in order to accept others configurations.
* @param operators The operators to validate.
* @throws IllegalArgumentException if the homonyms are not compatibles.
* @see #guessOperator(Token, List)
*/
protected void validateHomonyms(List<Operator> operators) {
if (operators.size()>2) {
throw new IllegalArgumentException();
}
}
/**
* Returns all fields that contains in the condition
* @param expression Condition expression
* @param resolver FormFieldResolver for the form
* @param isJoinClause indicates that condition is a join qualification
* @return List of IField
* @throws ParseException
*/
public List<IField<?>> getConditionFields(String expression, FormFieldResolver resolver,
boolean isJoinClause) throws ParseException {
List<IField<?>> fields = new ArrayList<IField<?>>();
final Iterator<String> tokens = tokenize(expression);
Token previous = null;
while (tokens.hasNext()) {
// read one token from the input stream
String strToken = tokens.next();
final Token token = toToken(previous, strToken, resolver, isJoinClause);
if (token.isField())
fields.add(token.getField());
}
return fields;
}
public String buildCondition(String expression, AbstractVariableSet<Value<?>> environment,
FormFieldResolver resolver, List<Value<?>> sorted) throws ParseException {
return buildCondition(expression, environment,
resolver, sorted, null, null, null, null, false);
}
/**
* Builds condition expression for prepared statement
* @param expression - Expression to be processed
* @param environment - Environment variables
* @param resolver - FormFieldResolver for the form
* @param sorted - [Out] Prepared sorted list of values for PreparedStatement
* @param leftAlias - left form alias
* @param rightAlias - right form alias
* @param leftFieldAlias - map contains left form field aliases
* @param rightFieldAlias - map contains right form field aliases
* @param isJoinClause - true if expression is join form condition, otherwise false
* @return
* @throws ParseException
*/
public String buildCondition(String expression, AbstractVariableSet<Value<?>> environment,
FormFieldResolver resolver, List<Value<?>> sorted, String leftAlias, String rightAlias,
Map<DBField, String> leftFieldAlias, Map<DBField, String> rightFieldAlias,
boolean isJoinClause) throws ParseException {
String condition = "";
final Iterator<String> tokens = tokenize(expression);
Token previous = null;
Token leftOperand = null;
int brackets = 0;
while (tokens.hasNext()) {
// read one token from the input stream
String strToken = tokens.next();
final Token token = toToken(previous, strToken, resolver, isJoinClause);
if (token.isOpenBracket()) {
// If the token is a left parenthesis, then push it onto the stack.
condition += "(";
brackets++;
if (previous!=null && previous.isFunction()) {
if (!functionBrackets.containsKey(token.getBrackets().getOpen())) {
throw new IllegalArgumentException("Invalid bracket after function: "+strToken);
}
} else {
if (!expressionBrackets.containsKey(token.getBrackets().getOpen())) {
throw new IllegalArgumentException("Invalid bracket in expression: "+strToken);
}
}
} else if (token.isCloseBracket()) {
if (previous==null || brackets==0) {
throw new IllegalArgumentException("expression can't start with a close bracket");
}
if (previous.isFunctionArgumentSeparator()) {
throw new IllegalArgumentException("argument is missing");
}
condition += ")";
brackets--;
} else if (token.isFunctionArgumentSeparator()) {
if (previous==null) {
throw new IllegalArgumentException("expression can't start with a function argument separator");
}
// Verify that there was an argument before this separator
if (previous.isOpenBracket() || previous.isFunctionArgumentSeparator()) {
// The cases were operator miss an operand are detected elsewhere.
throw new IllegalArgumentException("argument is missing");
}
// If the token is a function argument separator
condition+=",";
} else if (token.isFunction()) {
// If the token is a function token, then push it onto the stack.
condition+=token.getFunction().getName();
} else if (token.isOperator()) {
// If the token is an operator, op1, then:
condition+=" " + token.getOperator().getDbSymbol();
if (previous.isField())
leftOperand = previous;
} else if (token.isField()) {
// If token is a field
condition += convertFieldToSqlStatement(token, leftAlias, rightAlias,
leftFieldAlias, rightFieldAlias, isJoinClause, resolver);
} else {
// If the token is a number (identifier), a constant or a variable, then add its value to the output queue.
if ((previous!=null) && previous.isLiteral()) {
throw new IllegalArgumentException("A literal can't follow another literal");
}
Value<?> v = toValue(token, environment, previous, leftOperand);
if (v == null)
throw new IllegalArgumentException("Syntax error. Can't read value for: " + token.getLiteral());
if (v.getValue() == null) {
if (previous.getOperator().equals(EQ)) {
condition = condition.substring(0, condition.lastIndexOf(EQ.getDbSymbol()));
condition += " IS NULL";
} else if (previous.getOperator().equals(NOT_EQ)) {
condition = condition.substring(0, condition.lastIndexOf(NOT_EQ.getDbSymbol()));
condition += " IS NOT NULL";
} else {
condition += " ?";
sorted.add(v);
}
} else {
condition += " ?";
sorted.add(v);
}
//output(values, token, evaluationContext);
}
previous = token;
}
if (brackets != 0) {
throw new IllegalArgumentException("Invalid bracket in expression.");
}
return condition;
}
/**
*
* @param token
* @param leftAlias
* @param rightAlias
* @param leftFieldAlias
* @param rightFieldAlias
* @param rightFields
* @return
*/
private String convertFieldToSqlStatement(Token token, String leftAlias, String rightAlias,
Map<DBField, String> leftFieldAlias, Map<DBField, String> rightFieldAlias,
boolean isJoinClause, FormFieldResolver resolver) {
String res = null;
/*
* � ��� ����� ���� ��� �������� �������� �������:
* 1. ��� ������� �����
* 2. ��� ����� ����� � where ������� � ���
* 3. ��� ����� ����� � join ������� � ���
*/
// not a join condition
if (!isJoinClause) {
// basic form
if (!resolver.getForm().isJoin())
res=" " + token.getField().getDBField().getName();
else { // join form
// if field mapped to the left form
if (resolver.getLeftResolver().getForm().getName().equals(
token.getField().getForm())) {
res = " " + leftAlias + ".";
// if left form is join, it can have field alias
if (leftFieldAlias != null) {
String alias = leftFieldAlias.get(token.getField().getDBField());
if (alias != null)
res += alias;
else // if alias not found
res += token.getField().getDBField().getName();
} else // if alias map not specified, use table column name
res += token.getField().getDBField().getName();
} else {
res = " " + rightAlias + ".";
if (rightFieldAlias != null) {
String alias = rightFieldAlias.get(token.getField().getDBField());
if (alias != null)
res += alias;
else // if alias not found
res += token.getField().getDBField().getName();
} else // if alias map not specified, use table column name
res += token.getField().getDBField().getName();
}
}
}
else { // if form is a join form
if (token.isJoinField()) { // if field from the right form
res=" " + rightAlias + ".";
String alias = rightFieldAlias.get(token.getField().getDBField());
if (alias == null)
res += token.getField().getDBField().getName();
else
res += alias;
} else { // if field from the left form
res=" " + leftAlias + ".";
String alias = leftFieldAlias.get(token.getField().getDBField());
if (alias == null)
res += token.getField().getDBField().getName();
else
res += alias;
}
}
return res;
}
@SuppressWarnings("unchecked")
protected Value<?> toValue(Token token, Object evaluationContext,
Token previous, Token lastField) {
if (token.isLiteral()) { // If the token is a literal, a constant, or a variable name
String literal = token.getLiteral();
Constant ct = this.constants.get(literal);
Value<?> value = convert(ct, previous, lastField);
if (value==null && evaluationContext!=null && (evaluationContext instanceof AbstractVariableSet)) {
value = ((AbstractVariableSet<Value<?>>)evaluationContext).get(literal);
}
if (value != null) {
return value;
}
if (lastField != null && lastField.isField()) {
Value<?> v = convert(literal, lastField.getField());
if (v == null) {
throw new IllegalArgumentException("Incorrect value for field " +
lastField.getLiteral() + " value: " + literal);
}
return v;
}
} else {
throw new IllegalArgumentException();
}
return null;
}
protected Value<?> convert(String s, IField<?> field) {
return field.fromString(s);
}
protected Value<?> convert(Constant c, Token previous, Token lastField) {
if (NULL.equals(c) && lastField != null && lastField.isField()) {
return convert((String)null, lastField.getField());
}
return null;
}
protected Token toToken(Token previous, String token, FormFieldResolver resolver,
boolean isJoinClause) {
if (token.equals(functionArgumentSeparator)) {
return Token.FUNCTION_ARG_SEPARATOR;
} else if (functions.containsKey(token)) {
return Token.buildFunction(functions.get(token));
} else if (operators.containsKey(token)) {
List<Operator> list = operators.get(token);
return (list.size()==1) ? Token.buildOperator(list.get(0)) : Token.buildOperator(guessOperator(previous, list));
} else {
final BracketPair brackets = getBracketPair(token);
if (brackets!=null) {
if (brackets.getOpen().equals(token)) {
return Token.buildOpenToken(brackets);
} else {
return Token.buildCloseToken(brackets);
}
} else if (token.startsWith("'") && token.endsWith("'")) {
// it can be basic form field or left join form field
String fname = token.substring(1, token.length()-1);
Collection<IField<?>> fields = null;
// if it is join condition, we need resolve fields separately for each form
if (isJoinClause)
fields = resolver.getLeftResolver().getFields().values();
else
fields = resolver.getFields().values();
for (IField<?> f : fields) {
if (f.getId().equals(fname))
return Token.buildFieldToken(f);
}
throw new IllegalArgumentException("Field not found: " + fname);
} else if (token.startsWith("`") && token.endsWith("`")) {
String fname = token.substring(1, token.length()-1);
for (IField<?> f : resolver.getRightResolver().getFields().values()) {
if (f.getId().equals(fname))
return Token.buildJoinFieldToken(f);
}
throw new IllegalArgumentException("Field not found: " + fname);
} else if (token.startsWith("\"") && token.endsWith("\"")) {
String lit = token.substring(1, token.length()-1);
return Token.buildLiteral(lit);
} else {
return Token.buildLiteral(token);
}
}
}
/** When a token can be more than one operator (homonym operators), this method guesses the right operator.
* <br>A very common case is the - sign in arithmetic computation which can be an unary or a binary operator, depending
* on what was the previous token.
* <br><b>Warning:</b> maybe the arguments of this function are not enough to deal with all the cases.
* So, this part of the evaluation is in alpha state (method may change in the future).
* @param previous The last parsed tokens (the previous token in the infix expression we are evaluating).
* @param candidates The candidate tokens.
* @return A token
* @see #validateHomonyms(List)
*/
protected Operator guessOperator(Token previous, List<Operator> candidates) {
final int argCount = ((previous!=null) && (previous.isCloseBracket() || previous.isLiteral())) ? 2 : 1;
for (Operator operator : candidates) {
if (operator.getOperandCount()==argCount) {
return operator;
}
}
return null;
}
private BracketPair getBracketPair(String token) {
BracketPair result = expressionBrackets.get(token);
return result==null ? functionBrackets.get(token) : result;
}
/** Converts the evaluated expression into tokens.
* <br>Example: The result for the expression "<i>-1+min(10,3)</i>" is an iterator on "-", "1", "+", "min", "(", "10", ",", "3", ")".
* <br>By default, the operators symbols, the brackets and the function argument separator are used as delimiter in the string.
* @param expression The expression that is evaluated
* @return A string iterator.
* @throws ParseException
*/
protected Iterator<String> tokenize(String expression) throws ParseException {
return tokenizer.tokenize(expression);
}
/**
* Builds variables from fields
* @param fields List of fields
* @return
*/
public static AbstractVariableSet<Value<?>> buildVariablesFromFields(List<IField<?>> fields) {
StaticVariableSet<Value<?>> variables = new StaticVariableSet<Value<?>>();
for(IField<?> f : fields) {
variables.set("$" + f.getId() + "$", f.getDBValue());
}
return variables;
}
}