001/*
002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.directory.multi;
020
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.nuxeo.ecm.core.api.DocumentModelList;
030import org.nuxeo.ecm.core.query.QueryParseException;
031import org.nuxeo.ecm.core.query.sql.model.Expression;
032import org.nuxeo.ecm.core.query.sql.model.IdentityQueryTransformer;
033import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
034import org.nuxeo.ecm.core.query.sql.model.Operand;
035import org.nuxeo.ecm.core.query.sql.model.Operator;
036import org.nuxeo.ecm.core.query.sql.model.Predicate;
037import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
038import org.nuxeo.ecm.core.query.sql.model.Reference;
039import org.nuxeo.ecm.directory.Session;
040import org.nuxeo.ecm.directory.multi.MultiDirectorySession.SourceInfo;
041import org.nuxeo.ecm.directory.multi.MultiDirectorySession.SubDirectoryInfo;
042
043/**
044 * Evaluator for an {@link Expression} in the context of the various subdirectories of a MultiDirectory's source.
045 * <p>
046 * The result is a set of entry ids.
047 * <p>
048 * The strategy for evaluation is to delegate as much as possible of the evaluation of expressions to subdirectories
049 * themselves.
050 * <p>
051 * We do a depth-first evaluation of expressions, delaying actual evaluation while an expression's references all fall
052 * into the same subdirectory.
053 *
054 * @since 10.3
055 */
056public class MultiDirectoryExpressionEvaluator {
057
058    /** The result of an evaluation of an expression. */
059    public interface Result {
060    }
061
062    /** Result is a set of entry ids. */
063    public static class IdsResult implements Result {
064        public final Set<String> ids;
065
066        public IdsResult(Set<String> ids) {
067            this.ids = ids;
068        }
069    }
070
071    /** Result is an operand associated to at most one subdirectory. */
072    public static class OperandResult implements Result {
073
074        public final Operand operand;
075
076        public final boolean hasId;
077
078        public final String dir; // may be null
079
080        public OperandResult(Operand operand, boolean hasId, String dir) {
081            this.operand = operand;
082            this.hasId = hasId;
083            this.dir = dir;
084        }
085    }
086
087    protected final List<SubDirectoryInfo> dirInfos;
088
089    protected final String idField;
090
091    protected final String dirName; // for error messages
092
093    public MultiDirectoryExpressionEvaluator(SourceInfo sourceInfo, String idField, String dirName) {
094        dirInfos = sourceInfo.subDirectoryInfos;
095        this.idField = idField;
096        this.dirName = dirName;
097
098    }
099
100    /**
101     * Evaluates an expression and returns the set of matching ids.
102     */
103    public Set<String> eval(Expression expr) {
104        return evaluate(evalExpression(expr));
105    }
106
107    protected Result evalExpression(Expression expr) {
108        Operator op = expr.operator;
109        if (expr instanceof MultiExpression) {
110            return evalMultiExpression((MultiExpression) expr);
111        } else if (op == Operator.AND || op == Operator.OR) {
112            return evalAndOr(expr);
113        } else {
114            return evalSimpleExpression(expr);
115        }
116    }
117
118    protected Result evalSimpleExpression(Expression expr) {
119        Result left = evalOperand(expr.lvalue);
120        Result right = evalOperand(expr.rvalue);
121
122        // case where we can return a single-dir operand to the caller
123        if (left instanceof OperandResult && right instanceof OperandResult) {
124            // check id and subdirectories
125            OperandResult lop = (OperandResult) left;
126            OperandResult rop = (OperandResult) right;
127            if (lop.dir == null || rop.dir == null || lop.dir.equals(rop.dir)) {
128                // still one subdirectory
129                String dir = lop.dir == null ? rop.dir : lop.dir;
130                return new OperandResult(expr, lop.hasId || rop.hasId, dir);
131            }
132        }
133
134        // else for a simple expression we have no way of doing manual evaluation
135        throw new QueryParseException("Invalid expression for multidirectory: " + expr);
136    }
137
138    protected Result evalOperand(Operand op) {
139        if (op instanceof Expression) {
140            return evalExpression((Expression) op);
141        } else if (op instanceof Reference) {
142            return evalReference((Reference) op);
143        } else { // Literal / LiteralList / Function
144            return new OperandResult(op, false, null);
145        }
146    }
147
148    protected Result evalReference(Reference ref) {
149        String name = ref.name;
150        if (name.equals(idField)) {
151            return new OperandResult(ref, true, null);
152        }
153        for (SubDirectoryInfo dirInfo : dirInfos) {
154            if (dirInfo.fromSource.containsKey(name)) {
155                return new OperandResult(ref, false, dirInfo.dirName);
156            }
157        }
158        throw new QueryParseException("No column: " + name + " for directory: " + dirName);
159    }
160
161    protected Result evalAndOr(Expression expr) {
162        List<Predicate> predicates = Arrays.asList((Predicate) expr.lvalue, (Predicate) expr.rvalue);
163        return evalMultiExpression(new MultiExpression(expr.operator, predicates));
164    }
165
166    protected Result evalMultiExpression(MultiExpression expr) {
167        boolean and = expr.operator == Operator.AND;
168        List<Predicate> predicates = expr.predicates;
169        Iterator<Predicate> it = predicates.iterator();
170        if (!it.hasNext()) {
171            // empty multiexpression
172            return new OperandResult(expr, false, null);
173        }
174        Result previous = evalExpression(it.next());
175        while (it.hasNext()) {
176            if (and && previous instanceof IdsResult && ((IdsResult) previous).ids.isEmpty()) {
177                // optimization, no need to do more work
178                return previous;
179            }
180            Result next = evalExpression(it.next());
181
182            // check if we can keep a single-dir operand
183            if (previous instanceof OperandResult && next instanceof OperandResult) {
184                // check id and subdirectories
185                OperandResult prv = (OperandResult) previous;
186                OperandResult nxt = (OperandResult) next;
187                if (prv.dir == null || nxt.dir == null || prv.dir.equals(nxt.dir)) {
188                    // still one subdirectory
189                    String dir = prv.dir == null ? nxt.dir : prv.dir;
190                    previous = new OperandResult(expr, prv.hasId || nxt.hasId, dir);
191                    continue;
192                }
193            }
194
195            // turn everything into ids and do intersection/union
196            Set<String> previousIds = evaluate(previous);
197            if (and && previousIds.isEmpty()) {
198                // optimization, no need to do more work
199                return new IdsResult(previousIds);
200            }
201            Set<String> nextIds = evaluate(next);
202            Set<String> ids = and ? intersection(previousIds, nextIds) : union(previousIds, nextIds);
203            previous = new IdsResult(ids);
204        }
205        return previous;
206    }
207
208    /**
209     * Evaluates a result and returns the set of matching ids.
210     */
211    protected Set<String> evaluate(Result result) {
212        if (result instanceof IdsResult) {
213            return ((IdsResult) result).ids;
214        } else {
215            return evaluate((OperandResult) result);
216        }
217    }
218
219    /**
220     * Evaluates an operand associated to a single directory and returns the set of matching ids.
221     */
222    protected Set<String> evaluate(OperandResult opr) {
223        // find subdirectory to use
224        SubDirectoryInfo subDirInfo = null;
225        for (SubDirectoryInfo dirInfo : dirInfos) {
226            if (opr.dir != null) {
227                if (opr.dir.equals(dirInfo.dirName)) {
228                    subDirInfo = dirInfo;
229                    break;
230                }
231            } else {
232                // expression without any reference (except maybe id), pick any non-optional directory
233                if (!dirInfo.isOptional) {
234                    subDirInfo = dirInfo;
235                    break;
236                }
237            }
238        }
239        if (subDirInfo == null) {
240            throw new QueryParseException(
241                    "Configuration error: no non-optional subdirectory for multidirectory: " + dirName);
242        }
243        // map field names from multidirectory to subdirectory
244        Predicate predicate = new ReferenceRenamer(subDirInfo.fromSource).transform((Predicate) opr.operand);
245        QueryBuilder queryBuilder = new QueryBuilder();
246        if (predicate instanceof MultiExpression) {
247            queryBuilder.filter((MultiExpression) predicate);
248        } else {
249            queryBuilder.predicate(predicate);
250        }
251        try (Session s = subDirInfo.getSession()) {
252            return new HashSet<>(s.queryIds(queryBuilder));
253        }
254    }
255
256    /**
257     * Renames the references according to a map.
258     *
259     * @since 10.3
260     */
261    public static class ReferenceRenamer extends IdentityQueryTransformer {
262
263        protected final Map<String, String> map;
264
265        public ReferenceRenamer(Map<String, String> map) {
266            this.map = map;
267        }
268
269        @Override
270        public Reference transform(Reference node) {
271            String name = node.name;
272            String newName = map.getOrDefault(name, name);
273            if (newName.equals(name)) {
274                return node;
275            } else {
276                return new Reference(newName, node.cast, node.esHint);
277            }
278        }
279    }
280
281    /**
282     * Set union.
283     */
284    protected static Set<String> union(Set<String> a, Set<String> b) {
285        if (a.isEmpty()) {
286            return Collections.emptySet();
287        } else if (b.isEmpty()) {
288            return Collections.emptySet();
289        } else {
290            Set<String> set = new HashSet<>(a);
291            if (set.addAll(b)) {
292                return set;
293            } else {
294                return a; // optimization, don't return a new set if there was no change
295            }
296        }
297    }
298
299    /**
300     * Set intersection.
301     */
302    protected static Set<String> intersection(Set<String> a, Set<String> b) {
303        if (a.isEmpty()) {
304            return Collections.emptySet();
305        } else if (b.isEmpty()) {
306            return Collections.emptySet();
307        } else {
308            Set<String> set = new HashSet<>(a);
309            if (set.retainAll(b)) {
310                return set;
311            } else {
312                return a; // optimization, don't return a new set if there was no change
313            }
314        }
315    }
316
317}