001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.core.storage;
018
019import static java.lang.Boolean.FALSE;
020import static java.lang.Boolean.TRUE;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Calendar;
025import java.util.Collections;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.regex.Pattern;
030
031import org.apache.commons.lang.CharUtils;
032import org.nuxeo.ecm.core.query.QueryParseException;
033import org.nuxeo.ecm.core.query.sql.NXQL;
034import org.nuxeo.ecm.core.query.sql.model.BooleanLiteral;
035import org.nuxeo.ecm.core.query.sql.model.DateLiteral;
036import org.nuxeo.ecm.core.query.sql.model.DoubleLiteral;
037import org.nuxeo.ecm.core.query.sql.model.Expression;
038import org.nuxeo.ecm.core.query.sql.model.Function;
039import org.nuxeo.ecm.core.query.sql.model.IntegerLiteral;
040import org.nuxeo.ecm.core.query.sql.model.Literal;
041import org.nuxeo.ecm.core.query.sql.model.LiteralList;
042import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
043import org.nuxeo.ecm.core.query.sql.model.Operand;
044import org.nuxeo.ecm.core.query.sql.model.Operator;
045import org.nuxeo.ecm.core.query.sql.model.Predicate;
046import org.nuxeo.ecm.core.query.sql.model.Reference;
047import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
048
049/**
050 * Evaluator for an {@link Expression}.
051 *
052 * @since 5.9.4
053 */
054public abstract class ExpressionEvaluator {
055
056    /** pseudo NXQL to resolve ancestor ids. */
057    public static final String NXQL_ECM_ANCESTOR_IDS = "ecm:__ancestorIds";
058
059    /** pseudo NXQL to resolve internal path. */
060    public static final String NXQL_ECM_PATH = "ecm:__path";
061
062    /** pseudo NXQL to resolve read acls. */
063    public static final String NXQL_ECM_READ_ACL = "ecm:__read_acl";
064
065    /**
066     * Interface for a class that knows how to resolve a path into an id.
067     */
068    public interface PathResolver {
069        /**
070         * Returns the id for a given path.
071         *
072         * @param path the path
073         * @return the id, or {@code null} if not found
074         */
075        String getIdForPath(String path);
076    }
077
078    public final PathResolver pathResolver;
079
080    public final Set<String> principals;
081
082    public ExpressionEvaluator(PathResolver pathResolver, String[] principals) {
083        this.pathResolver = pathResolver;
084        this.principals = principals == null ? null : new HashSet<String>(Arrays.asList(principals));
085    }
086
087    public Object walkExpression(Expression expr) {
088        Operator op = expr.operator;
089        Operand lvalue = expr.lvalue;
090        Operand rvalue = expr.rvalue;
091        String name = lvalue instanceof Reference ? ((Reference) lvalue).name : null;
092        if (op == Operator.STARTSWITH) {
093            return walkStartsWith(lvalue, rvalue);
094        } else if (NXQL.ECM_PATH.equals(name)) {
095            return walkEcmPath(op, rvalue);
096        } else if (NXQL.ECM_ANCESTORID.equals(name)) {
097            return walkAncestorId(op, rvalue);
098        } else if (op == Operator.SUM) {
099            throw new UnsupportedOperationException("SUM");
100        } else if (op == Operator.SUB) {
101            throw new UnsupportedOperationException("SUB");
102        } else if (op == Operator.MUL) {
103            throw new UnsupportedOperationException("MUL");
104        } else if (op == Operator.DIV) {
105            throw new UnsupportedOperationException("DIV");
106        } else if (op == Operator.LT) {
107            return walkLt(lvalue, rvalue);
108        } else if (op == Operator.GT) {
109            return walkGt(lvalue, rvalue);
110        } else if (op == Operator.EQ) {
111            return walkEq(lvalue, rvalue);
112        } else if (op == Operator.NOTEQ) {
113            return walkNotEq(lvalue, rvalue);
114        } else if (op == Operator.LTEQ) {
115            return walkLtEq(lvalue, rvalue);
116        } else if (op == Operator.GTEQ) {
117            return walkGtEq(lvalue, rvalue);
118        } else if (op == Operator.AND) {
119            if (expr instanceof MultiExpression) {
120                return walkMultiExpression((MultiExpression) expr);
121            } else {
122                return walkAnd(lvalue, rvalue);
123            }
124        } else if (op == Operator.NOT) {
125            return walkNot(lvalue);
126        } else if (op == Operator.OR) {
127            return walkOr(lvalue, rvalue);
128        } else if (op == Operator.LIKE) {
129            return walkLike(lvalue, rvalue, true, false);
130        } else if (op == Operator.ILIKE) {
131            return walkLike(lvalue, rvalue, true, true);
132        } else if (op == Operator.NOTLIKE) {
133            return walkLike(lvalue, rvalue, false, false);
134        } else if (op == Operator.NOTILIKE) {
135            return walkLike(lvalue, rvalue, false, true);
136        } else if (op == Operator.IN) {
137            return walkIn(lvalue, rvalue, true);
138        } else if (op == Operator.NOTIN) {
139            return walkIn(lvalue, rvalue, false);
140        } else if (op == Operator.ISNULL) {
141            return walkIsNull(lvalue);
142        } else if (op == Operator.ISNOTNULL) {
143            return walkIsNotNull(lvalue);
144        } else if (op == Operator.BETWEEN) {
145            return walkBetween(lvalue, rvalue, true);
146        } else if (op == Operator.NOTBETWEEN) {
147            return walkBetween(lvalue, rvalue, false);
148        } else {
149            throw new QueryParseException("Unknown operator: " + op);
150        }
151    }
152
153    protected Boolean walkEcmPath(Operator op, Operand rvalue) {
154        if (op != Operator.EQ && op != Operator.NOTEQ) {
155            throw new QueryParseException(NXQL.ECM_PATH + " requires = or <> operator");
156        }
157        if (!(rvalue instanceof StringLiteral)) {
158            throw new QueryParseException(NXQL.ECM_PATH + " requires literal path as right argument");
159        }
160        String path = ((StringLiteral) rvalue).value;
161        if (path.length() > 1 && path.endsWith("/")) {
162            path = path.substring(0, path.length() - 1);
163        }
164        String id = pathResolver.getIdForPath(path);
165        if (id == null) {
166            return FALSE;
167        }
168        Boolean eq = eq(id, walkReference(new Reference(NXQL.ECM_UUID)));
169        return op == Operator.EQ ? eq : not(eq);
170    }
171
172    protected Boolean walkAncestorId(Operator op, Operand rvalue) {
173        if (op != Operator.EQ && op != Operator.NOTEQ) {
174            throw new QueryParseException(NXQL.ECM_ANCESTORID + " requires = or <> operator");
175        }
176        if (!(rvalue instanceof StringLiteral)) {
177            throw new QueryParseException(NXQL.ECM_ANCESTORID + " requires literal id as right argument");
178        }
179        String ancestorId = ((StringLiteral) rvalue).value;
180        Object[] ancestorIds = (Object[]) walkReference(new Reference(NXQL_ECM_ANCESTOR_IDS));
181        boolean eq = op == Operator.EQ ? true : false;
182        if (ancestorIds == null) {
183            // placeless
184            return eq ? FALSE : TRUE;
185        }
186        for (Object id : ancestorIds) {
187            if (ancestorId.equals(id)) {
188                return eq ? TRUE : FALSE;
189            }
190        }
191        return eq ? FALSE : TRUE;
192    }
193
194    public Boolean walkNot(Operand value) {
195        return not(bool(walkOperand(value)));
196    }
197
198    public Boolean walkIsNull(Operand value) {
199        return Boolean.valueOf(walkOperand(value) == null);
200    }
201
202    public Boolean walkIsNotNull(Operand value) {
203        return Boolean.valueOf(walkOperand(value) != null);
204    }
205
206    public Boolean walkMultiExpression(MultiExpression expr) {
207        Boolean res = TRUE;
208        for (Operand value : expr.values) {
209            Boolean bool = bool(walkOperand(value));
210            if (bool == null) {
211                // null is absorbent
212                return null;
213            }
214            res = and(res, bool);
215        }
216        return res;
217    }
218
219    public Boolean walkAnd(Operand lvalue, Operand rvalue) {
220        Boolean left = bool(walkOperand(lvalue));
221        Boolean right = bool(walkOperand(rvalue));
222        return and(left, right);
223    }
224
225    public Boolean walkOr(Operand lvalue, Operand rvalue) {
226        Boolean left = bool(walkOperand(lvalue));
227        Boolean right = bool(walkOperand(rvalue));
228        return or(left, right);
229    }
230
231    public Boolean walkEq(Operand lvalue, Operand rvalue) {
232        Object right = walkOperand(rvalue);
233        if (isMixinTypes(lvalue)) {
234            if (!(right instanceof String)) {
235                throw new QueryParseException("Invalid EQ rhs: " + rvalue);
236            }
237            return walkMixinTypes(Collections.singletonList((String) right), true);
238        }
239        Object left = walkOperand(lvalue);
240        return eqMaybeList(left, right);
241    }
242
243    public Boolean walkNotEq(Operand lvalue, Operand rvalue) {
244        if (isMixinTypes(lvalue)) {
245            Object right = walkOperand(rvalue);
246            if (!(right instanceof String)) {
247                throw new QueryParseException("Invalid NE rhs: " + rvalue);
248            }
249            return walkMixinTypes(Collections.singletonList((String) right), false);
250        }
251        return not(walkEq(lvalue, rvalue));
252    }
253
254    public Boolean walkLt(Operand lvalue, Operand rvalue) {
255        Integer cmp = cmp(lvalue, rvalue);
256        return cmp == null ? null : cmp < 0;
257    }
258
259    public Boolean walkGt(Operand lvalue, Operand rvalue) {
260        Integer cmp = cmp(lvalue, rvalue);
261        return cmp == null ? null : cmp > 0;
262    }
263
264    public Boolean walkLtEq(Operand lvalue, Operand rvalue) {
265        Integer cmp = cmp(lvalue, rvalue);
266        return cmp == null ? null : cmp <= 0;
267    }
268
269    public Boolean walkGtEq(Operand lvalue, Operand rvalue) {
270        Integer cmp = cmp(lvalue, rvalue);
271        return cmp == null ? null : cmp >= 0;
272    }
273
274    public Object walkBetween(Operand lvalue, Operand rvalue, boolean positive) {
275        LiteralList l = (LiteralList) rvalue;
276        Predicate va = new Predicate(lvalue, Operator.GTEQ, l.get(0));
277        Predicate vb = new Predicate(lvalue, Operator.LTEQ, l.get(1));
278        Predicate pred = new Predicate(va, Operator.AND, vb);
279        if (!positive) {
280            pred = new Predicate(pred, Operator.NOT, null);
281        }
282        return walkExpression(pred);
283    }
284
285    public Boolean walkIn(Operand lvalue, Operand rvalue, boolean positive) {
286        Object right = walkOperand(rvalue);
287        if (!(right instanceof List)) {
288            throw new QueryParseException("Invalid IN rhs: " + rvalue);
289        }
290        if (isMixinTypes(lvalue)) {
291            return walkMixinTypes((List<String>) right, positive);
292        }
293        Object left = walkOperand(lvalue);
294        Boolean in = inMaybeList(left, (List<Object>) right);
295        return positive ? in : not(in);
296    }
297
298    public Object walkOperand(Operand op) {
299        if (op instanceof Literal) {
300            return walkLiteral((Literal) op);
301        } else if (op instanceof LiteralList) {
302            return walkLiteralList((LiteralList) op);
303        } else if (op instanceof Function) {
304            return walkFunction((Function) op);
305        } else if (op instanceof Expression) {
306            return walkExpression((Expression) op);
307        } else if (op instanceof Reference) {
308            return walkReference((Reference) op);
309        } else {
310            throw new QueryParseException("Unknown operand: " + op);
311        }
312    }
313
314    public Object walkLiteral(Literal lit) {
315        if (lit instanceof BooleanLiteral) {
316            return walkBooleanLiteral((BooleanLiteral) lit);
317        } else if (lit instanceof DateLiteral) {
318            return walkDateLiteral((DateLiteral) lit);
319        } else if (lit instanceof DoubleLiteral) {
320            return walkDoubleLiteral((DoubleLiteral) lit);
321        } else if (lit instanceof IntegerLiteral) {
322            return walkIntegerLiteral((IntegerLiteral) lit);
323        } else if (lit instanceof StringLiteral) {
324            return walkStringLiteral((StringLiteral) lit);
325        } else {
326            throw new QueryParseException("Unknown literal: " + lit);
327        }
328    }
329
330    public Boolean walkBooleanLiteral(BooleanLiteral lit) {
331        return Boolean.valueOf(lit.value);
332    }
333
334    public Calendar walkDateLiteral(DateLiteral lit) {
335        return lit.toCalendar(); // TODO onlyDate
336    }
337
338    public Double walkDoubleLiteral(DoubleLiteral lit) {
339        return Double.valueOf(lit.value);
340    }
341
342    public Long walkIntegerLiteral(IntegerLiteral lit) {
343        return Long.valueOf(lit.value);
344    }
345
346    public String walkStringLiteral(StringLiteral lit) {
347        return lit.value;
348    }
349
350    public List<Object> walkLiteralList(LiteralList litList) {
351        List<Object> list = new ArrayList<Object>(litList.size());
352        for (Literal lit : litList) {
353            list.add(walkLiteral(lit));
354        }
355        return list;
356    }
357
358    public Boolean walkLike(Operand lvalue, Operand rvalue, boolean positive, boolean caseInsensitive) {
359        Object left = walkOperand(lvalue);
360        Object right = walkOperand(rvalue);
361        if (!(right instanceof String)) {
362            throw new QueryParseException("Invalid LIKE rhs: " + rvalue);
363        }
364        return likeMaybeList(left, (String) right, positive, caseInsensitive);
365    }
366
367    public Object walkFunction(Function func) {
368        throw new UnsupportedOperationException("Function");
369    }
370
371    public Boolean walkStartsWith(Operand lvalue, Operand rvalue) {
372        if (!(lvalue instanceof Reference)) {
373            throw new QueryParseException("Invalid STARTSWITH query, left hand side must be a property: " + lvalue);
374        }
375        String name = ((Reference) lvalue).name;
376        if (!(rvalue instanceof StringLiteral)) {
377            throw new QueryParseException("Invalid STARTSWITH query, right hand side must be a literal path: " + rvalue);
378        }
379        String path = ((StringLiteral) rvalue).value;
380        if (path.length() > 1 && path.endsWith("/")) {
381            path = path.substring(0, path.length() - 1);
382        }
383
384        if (NXQL.ECM_PATH.equals(name)) {
385            return walkStartsWithPath(path);
386        } else {
387            return walkStartsWithNonPath(lvalue, path);
388        }
389    }
390
391    protected Boolean walkStartsWithPath(String path) {
392        // resolve path
393        String ancestorId = pathResolver.getIdForPath(path);
394        if (ancestorId == null) {
395            // no such path
396            return FALSE;
397        }
398        Object[] ancestorIds = (Object[]) walkReference(new Reference(NXQL_ECM_ANCESTOR_IDS));
399        if (ancestorIds == null) {
400            // placeless
401            return FALSE;
402        }
403        for (Object id : ancestorIds) {
404            if (ancestorId.equals(id)) {
405                return TRUE;
406            }
407        }
408        return FALSE;
409    }
410
411    protected Boolean walkStartsWithNonPath(Operand lvalue, String path) {
412        Object left = walkReference((Reference) lvalue);
413        // exact match
414        Boolean bool = eqMaybeList(left, path);
415        if (TRUE.equals(bool)) {
416            return TRUE;
417        }
418        // prefix match TODO escape % chars
419        String pattern = path + "/%";
420        return likeMaybeList(left, pattern, true, false);
421    }
422
423    /**
424     * Evaluates a reference over the context state.
425     *
426     * @param ref the reference
427     */
428    public abstract Object walkReference(Reference ref);
429
430    protected boolean isMixinTypes(Operand op) {
431        if (!(op instanceof Reference)) {
432            return false;
433        }
434        return ((Reference) op).name.equals(NXQL.ECM_MIXINTYPE);
435    }
436
437    protected Boolean bool(Object value) {
438        if (value == null) {
439            return null;
440        }
441        if (!(value instanceof Boolean)) {
442            throw new QueryParseException("Not a boolean: " + value);
443        }
444        return (Boolean) value;
445    }
446
447    // ternary logic
448    protected Boolean not(Boolean value) {
449        if (value == null) {
450            return null;
451        }
452        return !value;
453    }
454
455    // ternary logic
456    protected Boolean and(Boolean left, Boolean right) {
457        if (TRUE.equals(left)) {
458            return right;
459        } else {
460            return left;
461        }
462    }
463
464    // ternary logic
465    protected Boolean or(Boolean left, Boolean right) {
466        if (TRUE.equals(left)) {
467            return left;
468        } else {
469            return right;
470        }
471    }
472
473    // ternary logic
474    protected Boolean eq(Object left, Object right) {
475        if (left == null || right == null) {
476            return null;
477        }
478        return left.equals(right);
479    }
480
481    // ternary logic
482    protected Boolean in(Object left, List<Object> right) {
483        if (left == null) {
484            return null;
485        }
486        boolean hasNull = false;
487        for (Object r : right) {
488            if (r == null) {
489                hasNull = true;
490            } else if (left.equals(r)) {
491                return TRUE;
492            }
493        }
494        return hasNull ? null : FALSE;
495    }
496
497    protected Integer cmp(Operand lvalue, Operand rvalue) {
498        Object left = walkOperand(lvalue);
499        Object right = walkOperand(rvalue);
500        return cmp(left, right);
501    }
502
503    // ternary logic
504    protected Integer cmp(Object left, Object right) {
505        if (left == null || right == null) {
506            return null;
507        }
508        if (!(left instanceof Comparable)) {
509            throw new QueryParseException("Not a comparable: " + left);
510        }
511        return ((Comparable<Object>) left).compareTo(right);
512    }
513
514    // ternary logic
515    protected Boolean like(Object left, String right, boolean caseInsensitive) {
516        if (left == null || right == null) {
517            return null;
518        }
519        if (!(left instanceof String)) {
520            throw new QueryParseException("Invalid LIKE lhs: " + left);
521        }
522        String value = (String) left;
523        if (caseInsensitive) {
524            value = value.toLowerCase();
525            right = right.toLowerCase();
526        }
527        String regex = likeToRegex(right);
528        boolean match = Pattern.matches(regex.toString(), value);
529        return match;
530    }
531
532    /**
533     * Turns a NXQL LIKE pattern into a regex.
534     * <p>
535     * % and _ are standard wildcards, and \ escapes them.
536     *
537     * @since 7.4
538     */
539    public static String likeToRegex(String like) {
540        StringBuilder regex = new StringBuilder();
541        char[] chars = like.toCharArray();
542        boolean escape = false;
543        for (int i = 0; i < chars.length; i++) {
544            char c = chars[i];
545            boolean escapeNext = false;
546            switch (c) {
547            case '%':
548                if (escape) {
549                    regex.append(c);
550                } else {
551                    regex.append(".*");
552                }
553                break;
554            case '_':
555                if (escape) {
556                    regex.append(c);
557                } else {
558                    regex.append(".");
559                }
560                break;
561            case '\\':
562                if (escape) {
563                    regex.append("\\\\"); // backslash escaped for regexp
564                } else {
565                    escapeNext = true;
566                }
567                break;
568            default:
569                // escape mostly everything just in case
570                if (!CharUtils.isAsciiAlphanumeric(c)) {
571                    regex.append("\\");
572                }
573                regex.append(c);
574                break;
575            }
576            escape = escapeNext;
577        }
578        if (escape) {
579            // invalid string terminated by escape character, ignore
580        }
581        return regex.toString();
582    }
583
584    // if list, use EXIST (SELECT 1 FROM left WHERE left.item = right)
585    protected Boolean eqMaybeList(Object left, Object right) {
586        if (left instanceof Object[]) {
587            for (Object l : ((Object[]) left)) {
588                Boolean eq = eq(l, right);
589                if (TRUE.equals(eq)) {
590                    return TRUE;
591                }
592            }
593            return FALSE;
594        } else {
595            return eq(left, right);
596        }
597    }
598
599    // if list, use EXIST (SELECT 1 FROM left WHERE left.item IN right)
600    protected Boolean inMaybeList(Object left, List<Object> right) {
601        if (left instanceof Object[]) {
602            for (Object l : ((Object[]) left)) {
603                Boolean in = in(l, right);
604                if (TRUE.equals(in)) {
605                    return TRUE;
606                }
607            }
608            return FALSE;
609        } else {
610            return in(left, right);
611        }
612    }
613
614    protected Boolean likeMaybeList(Object left, String right, boolean positive, boolean caseInsensitive) {
615        if (left instanceof Object[]) {
616            for (Object l : ((Object[]) left)) {
617                Boolean like = like(l, right, caseInsensitive);
618                if (TRUE.equals(like)) {
619                    return Boolean.valueOf(positive);
620                }
621            }
622            return Boolean.valueOf(!positive);
623        } else {
624            Boolean like = like(left, right, caseInsensitive);
625            return positive ? like : not(like);
626        }
627    }
628
629    /**
630     * Matches the mixin types against a list of values.
631     * <p>
632     * Used for:
633     * <ul>
634     * <li>ecm:mixinTypes = 'foo'
635     * <li>ecm:mixinTypes != 'foo'
636     * <li>ecm:mixinTypes IN ('foo', 'bar')
637     * <li>ecm:mixinTypes NOT IN ('foo', 'bar')
638     * </ul>
639     *
640     * @param mixins the mixin(s) to match
641     * @param include {@code true} for = and IN
642     * @since 7.4
643     */
644    public abstract Boolean walkMixinTypes(List<String> mixins, boolean include);
645
646}