/*
* (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.query.sql.model.BooleanLiteral;
import org.nuxeo.ecm.core.query.sql.model.DateLiteral;
import org.nuxeo.ecm.core.query.sql.model.DoubleLiteral;
import org.nuxeo.ecm.core.query.sql.model.Expression;
import org.nuxeo.ecm.core.query.sql.model.Function;
import org.nuxeo.ecm.core.query.sql.model.IntegerLiteral;
import org.nuxeo.ecm.core.query.sql.model.Literal;
import org.nuxeo.ecm.core.query.sql.model.LiteralList;
import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
import org.nuxeo.ecm.core.query.sql.model.Operand;
import org.nuxeo.ecm.core.query.sql.model.Operator;
import org.nuxeo.ecm.core.query.sql.model.Reference;
import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
/**
* Evaluator for an {@link Expression}.
*
* @since 5.9.4
*/
public abstract class ExpressionEvaluator {
/** pseudo NXQL to resolve ancestor ids. */
public static final String NXQL_ECM_ANCESTOR_IDS = "ecm:__ancestorIds";
/** pseudo NXQL to resolve internal path. */
public static final String NXQL_ECM_PATH = "ecm:__path";
/** pseudo NXQL to resolve read acls. */
public static final String NXQL_ECM_READ_ACL = "ecm:__read_acl";
/**
* Interface for a class that knows how to resolve a path into an id.
*/
public interface PathResolver {
/**
* Returns the id for a given path.
*
* @param path the path
* @return the id, or {@code null} if not found
*/
String getIdForPath(String path);
}
public final PathResolver pathResolver;
public final Set<String> principals;
public ExpressionEvaluator(PathResolver pathResolver, String[] principals) {
this.pathResolver = pathResolver;
this.principals = principals == null ? null : new HashSet<String>(
Arrays.asList(principals));
}
public Object walkExpression(Expression expr) {
Operator op = expr.operator;
Operand lvalue = expr.lvalue;
Operand rvalue = expr.rvalue;
String name = lvalue instanceof Reference ? ((Reference) lvalue).name
: null;
if (op == Operator.STARTSWITH) {
return walkStartsWith(lvalue, rvalue);
} else if (NXQL.ECM_PATH.equals(name)) {
return walkEcmPath(op, rvalue);
} else if (NXQL.ECM_ANCESTORID.equals(name)) {
return walkAncestorId(op, rvalue);
} else if (op == Operator.SUM) {
throw new UnsupportedOperationException("SUM");
} else if (op == Operator.SUB) {
throw new UnsupportedOperationException("SUB");
} else if (op == Operator.MUL) {
throw new UnsupportedOperationException("MUL");
} else if (op == Operator.DIV) {
throw new UnsupportedOperationException("DIV");
} else if (op == Operator.LT) {
return walkLt(lvalue, rvalue);
} else if (op == Operator.GT) {
return walkGt(lvalue, rvalue);
} else if (op == Operator.EQ) {
return walkEq(lvalue, rvalue);
} else if (op == Operator.NOTEQ) {
return walkNotEq(lvalue, rvalue);
} else if (op == Operator.LTEQ) {
return walkLtEq(lvalue, rvalue);
} else if (op == Operator.GTEQ) {
return walkGtEq(lvalue, rvalue);
} else if (op == Operator.AND) {
if (expr instanceof MultiExpression) {
return walkMultiExpression((MultiExpression) expr);
} else {
return walkAnd(lvalue, rvalue);
}
} else if (op == Operator.NOT) {
return walkNot(lvalue);
} else if (op == Operator.OR) {
return walkOr(lvalue, rvalue);
} else if (op == Operator.LIKE) {
return walkLike(lvalue, rvalue, true, false);
} else if (op == Operator.ILIKE) {
return walkLike(lvalue, rvalue, true, true);
} else if (op == Operator.NOTLIKE) {
return walkLike(lvalue, rvalue, false, false);
} else if (op == Operator.NOTILIKE) {
return walkLike(lvalue, rvalue, false, true);
} else if (op == Operator.IN) {
return walkIn(lvalue, rvalue, true);
} else if (op == Operator.NOTIN) {
return walkIn(lvalue, rvalue, false);
} else if (op == Operator.ISNULL) {
return walkIsNull(lvalue);
} else if (op == Operator.ISNOTNULL) {
return walkIsNotNull(lvalue);
} else if (op == Operator.BETWEEN) {
throw new UnsupportedOperationException("BETWEEN");
} else if (op == Operator.NOTBETWEEN) {
throw new UnsupportedOperationException("NOT BETWEEN");
} else {
throw new RuntimeException("Unknown operator: " + op);
}
}
protected Boolean walkEcmPath(Operator op, Operand rvalue) {
if (op != Operator.EQ && op != Operator.NOTEQ) {
throw new RuntimeException(NXQL.ECM_PATH
+ " requires = or <> operator");
}
if (!(rvalue instanceof StringLiteral)) {
throw new RuntimeException(NXQL.ECM_PATH
+ " requires literal path as right argument");
}
String path = ((StringLiteral) rvalue).value;
if (path.length() > 1 && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
String id = pathResolver.getIdForPath(path);
if (id == null) {
return FALSE;
}
Boolean eq = eq(id, walkReference(new Reference(NXQL.ECM_UUID)));
return op == Operator.EQ ? eq : not(eq);
}
protected Boolean walkAncestorId(Operator op, Operand rvalue) {
if (op != Operator.EQ && op != Operator.NOTEQ) {
throw new RuntimeException(NXQL.ECM_ANCESTORID
+ " requires = or <> operator");
}
if (!(rvalue instanceof StringLiteral)) {
throw new RuntimeException(NXQL.ECM_ANCESTORID
+ " requires literal id as right argument");
}
String ancestorId = ((StringLiteral) rvalue).value;
Object[] ancestorIds = (Object[]) walkReference(new Reference(
NXQL_ECM_ANCESTOR_IDS));
boolean eq = op == Operator.EQ ? true : false;
if (ancestorIds == null) {
// placeless
return eq ? FALSE : TRUE;
}
for (Object id : ancestorIds) {
if (ancestorId.equals(id)) {
return eq ? TRUE : FALSE;
}
}
return eq ? FALSE : TRUE;
}
public Boolean walkNot(Operand value) {
return not(bool(walkOperand(value)));
}
public Boolean walkIsNull(Operand value) {
return Boolean.valueOf(walkOperand(value) == null);
}
public Boolean walkIsNotNull(Operand value) {
return Boolean.valueOf(walkOperand(value) != null);
}
public Boolean walkMultiExpression(MultiExpression expr) {
Boolean res = TRUE;
for (Operand value : expr.values) {
Boolean bool = bool(walkOperand(value));
if (bool == null) {
// null is absorbent
return null;
}
res = and(res, bool);
}
return res;
}
public Boolean walkAnd(Operand lvalue, Operand rvalue) {
Boolean left = bool(walkOperand(lvalue));
Boolean right = bool(walkOperand(rvalue));
return and(left, right);
}
public Boolean walkOr(Operand lvalue, Operand rvalue) {
Boolean left = bool(walkOperand(lvalue));
Boolean right = bool(walkOperand(rvalue));
return or(left, right);
}
public Boolean walkEq(Operand lvalue, Operand rvalue) {
Object left = walkOperand(lvalue);
Object right = walkOperand(rvalue);
return eqMaybeList(left, right);
}
public Boolean walkNotEq(Operand lvalue, Operand rvalue) {
return not(walkEq(lvalue, rvalue));
}
public Boolean walkLt(Operand lvalue, Operand rvalue) {
Integer cmp = cmp(lvalue, rvalue);
return cmp == null ? null : cmp < 0;
}
public Boolean walkGt(Operand lvalue, Operand rvalue) {
Integer cmp = cmp(lvalue, rvalue);
return cmp == null ? null : cmp > 0;
}
public Boolean walkLtEq(Operand lvalue, Operand rvalue) {
Integer cmp = cmp(lvalue, rvalue);
return cmp == null ? null : cmp <= 0;
}
public Boolean walkGtEq(Operand lvalue, Operand rvalue) {
Integer cmp = cmp(lvalue, rvalue);
return cmp == null ? null : cmp >= 0;
}
public Boolean walkIn(Operand lvalue, Operand rvalue, boolean positive) {
Object left = walkOperand(lvalue);
Object right = walkOperand(rvalue);
if (!(right instanceof List)) {
throw new RuntimeException("Invalid IN rhs: " + rvalue);
}
Boolean in = inMaybeList(left, (List<Object>) right);
return positive ? in : not(in);
}
public Object walkOperand(Operand op) {
if (op instanceof Literal) {
return walkLiteral((Literal) op);
} else if (op instanceof LiteralList) {
return walkLiteralList((LiteralList) op);
} else if (op instanceof Function) {
return walkFunction((Function) op);
} else if (op instanceof Expression) {
return walkExpression((Expression) op);
} else if (op instanceof Reference) {
return walkReference((Reference) op);
} else {
throw new RuntimeException("Unknown operand: " + op);
}
}
public Object walkLiteral(Literal lit) {
if (lit instanceof BooleanLiteral) {
return walkBooleanLiteral((BooleanLiteral) lit);
} else if (lit instanceof DateLiteral) {
return walkDateLiteral((DateLiteral) lit);
} else if (lit instanceof DoubleLiteral) {
return walkDoubleLiteral((DoubleLiteral) lit);
} else if (lit instanceof IntegerLiteral) {
return walkIntegerLiteral((IntegerLiteral) lit);
} else if (lit instanceof StringLiteral) {
return walkStringLiteral((StringLiteral) lit);
} else {
throw new RuntimeException("Unknown literal: " + lit);
}
}
public Boolean walkBooleanLiteral(BooleanLiteral lit) {
return Boolean.valueOf(lit.value);
}
public Calendar walkDateLiteral(DateLiteral lit) {
return lit.toCalendar(); // TODO onlyDate
}
public Double walkDoubleLiteral(DoubleLiteral lit) {
return Double.valueOf(lit.value);
}
public Long walkIntegerLiteral(IntegerLiteral lit) {
return Long.valueOf(lit.value);
}
public String walkStringLiteral(StringLiteral lit) {
return lit.value;
}
public List<Object> walkLiteralList(LiteralList litList) {
List<Object> list = new ArrayList<Object>(litList.size());
for (Literal lit : litList) {
list.add(walkLiteral(lit));
}
return list;
}
public Boolean walkLike(Operand lvalue, Operand rvalue, boolean positive,
boolean caseInsensitive) {
Object left = walkOperand(lvalue);
Object right = walkOperand(rvalue);
if (!(right instanceof String)) {
throw new RuntimeException("Invalid LIKE rhs: " + rvalue);
}
return likeMaybeList(left, (String) right, positive, caseInsensitive);
}
public Object walkFunction(Function func) {
throw new UnsupportedOperationException("Function");
}
public Boolean walkStartsWith(Operand lvalue, Operand rvalue) {
if (!(lvalue instanceof Reference)) {
throw new RuntimeException(
"Invalid STARTSWITH query, left hand side must be a property: "
+ lvalue);
}
String name = ((Reference) lvalue).name;
if (!(rvalue instanceof StringLiteral)) {
throw new RuntimeException(
"Invalid STARTSWITH query, right hand side must be a literal path: "
+ rvalue);
}
String path = ((StringLiteral) rvalue).value;
if (path.length() > 1 && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
if (NXQL.ECM_PATH.equals(name)) {
return walkStartsWithPath(path);
} else {
return walkStartsWithNonPath(lvalue, path);
}
}
protected Boolean walkStartsWithPath(String path) {
// resolve path
String ancestorId = pathResolver.getIdForPath(path);
if (ancestorId == null) {
// no such path
return FALSE;
}
Object[] ancestorIds = (Object[]) walkReference(new Reference(
NXQL_ECM_ANCESTOR_IDS));
if (ancestorIds == null) {
// placeless
return FALSE;
}
for (Object id : ancestorIds) {
if (ancestorId.equals(id)) {
return TRUE;
}
}
return FALSE;
}
protected Boolean walkStartsWithNonPath(Operand lvalue, String path) {
Object left = walkReference((Reference) lvalue);
// exact match
Boolean bool = eqMaybeList(left, path);
if (TRUE.equals(bool)) {
return TRUE;
}
// prefix match TODO escape % chars
String pattern = path + "/%";
return likeMaybeList(left, pattern, true, false);
}
/**
* Evaluates a reference over the context state.
*
* @param ref the reference
*/
public abstract Object walkReference(Reference ref);
/**
* Evaluates a reference over the given state.
*
* @param ref the reference
* @param map the state representation
*/
public abstract Object evaluateReference(Reference ref, State map);
protected Boolean bool(Object value) {
if (value == null) {
return null;
}
if (!(value instanceof Boolean)) {
throw new RuntimeException("Not a boolean: " + value);
}
return (Boolean) value;
}
// ternary logic
protected Boolean not(Boolean value) {
if (value == null) {
return null;
}
return !value;
}
// ternary logic
protected Boolean and(Boolean left, Boolean right) {
if (TRUE.equals(left)) {
return right;
} else {
return left;
}
}
// ternary logic
protected Boolean or(Boolean left, Boolean right) {
if (TRUE.equals(left)) {
return left;
} else {
return right;
}
}
// ternary logic
protected Boolean eq(Object left, Object right) {
if (left == null || right == null) {
return null;
}
return left.equals(right);
}
// ternary logic
protected Boolean in(Object left, List<Object> right) {
if (left == null) {
return null;
}
boolean hasNull = false;
for (Object r : right) {
if (r == null) {
hasNull = true;
} else if (left.equals(r)) {
return TRUE;
}
}
return hasNull ? null : FALSE;
}
protected Integer cmp(Operand lvalue, Operand rvalue) {
Object left = walkOperand(lvalue);
Object right = walkOperand(rvalue);
return cmp(left, right);
}
// ternary logic
protected Integer cmp(Object left, Object right) {
if (left == null || right == null) {
return null;
}
if (!(left instanceof Comparable)) {
throw new RuntimeException("Not a comparable: " + left);
}
return ((Comparable<Object>) left).compareTo(right);
}
// ternary logic
protected Boolean like(Object left, String right, boolean caseInsensitive) {
if (left == null || right == null) {
return null;
}
if (!(left instanceof String)) {
throw new RuntimeException("Invalid LIKE lhs: " + left);
}
String value = (String) left;
if (caseInsensitive) {
value = value.toLowerCase();
right = right.toLowerCase();
}
// escape with slash except alphanumeric and percent
String regex = right.replaceAll("([^a-zA-Z0-9%])", "\\\\$1");
// replace percent with regexp
regex = regex.replaceAll("%", ".*");
boolean match = Pattern.compile(regex).matcher(value).matches();
return match;
}
// if list, use EXIST (SELECT 1 FROM left WHERE left.item = right)
protected Boolean eqMaybeList(Object left, Object right) {
if (left instanceof Object[]) {
for (Object l : ((Object[]) left)) {
Boolean eq = eq(l, right);
if (TRUE.equals(eq)) {
return TRUE;
}
}
return FALSE;
} else {
return eq(left, right);
}
}
// if list, use EXIST (SELECT 1 FROM left WHERE left.item IN right)
protected Boolean inMaybeList(Object left, List<Object> right) {
if (left instanceof Object[]) {
for (Object l : ((Object[]) left)) {
Boolean in = in(l, right);
if (TRUE.equals(in)) {
return TRUE;
}
}
return FALSE;
} else {
return in(left, right);
}
}
protected Boolean likeMaybeList(Object left, String right,
boolean positive, boolean caseInsensitive) {
if (left instanceof Object[]) {
for (Object l : ((Object[]) left)) {
Boolean like = like(l, right, caseInsensitive);
if (TRUE.equals(like)) {
return Boolean.valueOf(positive);
}
}
return Boolean.valueOf(!positive);
} else {
Boolean like = like(left, right, caseInsensitive);
return positive ? like : not(like);
}
}
}